validation_kit 1.0.5 → 2.0.1

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: ff3e9944d7507b5b68ac90c343eb6e95ee9c8219b9f210dcdf843352c72ac39d
4
- data.tar.gz: 57c6162bd4b136b247c7c15857fcde816e65a592a5dfcff2f793bb8719fb060a
3
+ metadata.gz: 83f8486330f7a2179f4655e0cd938355d9b79c9b96279238829b4c3c7a860151
4
+ data.tar.gz: 479ec62978042d99be1efb5985c3efad7e18615f5450374cd131ebd2a907ff06
5
5
  SHA512:
6
- metadata.gz: 686fc83cb5b88630d36ff2ceccc260e889f6800100700d8420b6a49d2702ffe5887892016fe219126ada9b656b2d671ca00f6fb991c5c5ba2dad0be84bba3016
7
- data.tar.gz: 831b93a7ec64043a9aaf43e2b050d4a93c0f050cda66dd378764857aad36bb09ffeba0fc59086dd19e2581e2227a9cd02c62c5f46f6bedd55dcbbe952d45accd
6
+ metadata.gz: b2940c397a76ebedbcbcb9802081159eb58b837c7284a6f77e5487081164b23447a30092485b7e765c295af88e8fdb6ba6fd5aac355cf46a1795b7beaad451fd
7
+ data.tar.gz: 5d59577b6a5d73bb96f1063b1d8b8125856d23d4d48d01c7e67584a47067e3df43054db60aa768c4c1b2bf175eea8fff8198000f8d1f0262b2175b3470c59d25
data/CHANGELOG.md ADDED
@@ -0,0 +1,150 @@
1
+ # Changelog
2
+
3
+ ## 2.0.1 (2026-10-02)
4
+
5
+ ### Breaking changes
6
+
7
+ - The email length limits (254 characters in all, 64 before the `@`) now count
8
+ every character, as RFC 5321 does, including the quotes around a quoted local
9
+ part and each backslash escape. So a quoted local part of 63 characters plus
10
+ its quotes, which used to pass, is now too long. The limits are checked by
11
+ the validator and the new `ValidationKit::EmailValidator.valid_address?`, not
12
+ by `EMAIL_ADDRESS_RE`: code that uses the regex directly no longer gets them,
13
+ and should call `valid_address?` instead.
14
+ - An `area_key:` that isn't an Australian state or territory (NSW, ACT, VIC,
15
+ TAS, QLD, SA, NT, WA, in any case) now raises `ArgumentError` when the model
16
+ is defined, and `format_as_phone` raises for one too. It used to silently add
17
+ the NSW/ACT area code (02), so `"Victoria"` or a typo stored a wrong number.
18
+ Leaving `area_key:` out still means NSW/ACT.
19
+ - A symbol `country:` option that's neither a method on the model nor a
20
+ two-letter country code (such as a misspelled method name,
21
+ `country: :billing_contry`) now raises `ArgumentError` when validating. It
22
+ used to fall back to the model's `country` method, or skip validation
23
+ silently.
24
+ - The bundled mixed case messages no longer start with the attribute's name, so
25
+ `full_messages` names it once ("First name cannot be in all caps", not "First
26
+ name First name cannot be in all caps"), like Rails' own messages:
27
+ - `errors[:first_name]` is now `["cannot be in all caps"]`. Code that shows
28
+ `errors[attribute]` on its own should use `full_messages_for` instead.
29
+ - `attribute_name:` no longer changes the default message. `full_messages`
30
+ uses the attribute's translated name (`human_attribute_name`) instead; to
31
+ rename it, translate the attribute. `attribute_name:` still fills in
32
+ `%{item}` in custom messages and translations, which keep working.
33
+ - Apps that set `errors.format` to `"%{message}"` to hide the doubled name now
34
+ get messages with no name at all, and can drop that setting.
35
+
36
+ ### Changed
37
+
38
+ - US and Canadian phone numbers with extra digits right after the number and no
39
+ extension marker in front of them (any letter, as in `x`, `ext` or
40
+ `extension`, or `#`, `#` or `№`) are now invalid, with or without `set:`. A
41
+ mistyped number such as `212-555-12345` used to pass, and `set:` stored it as
42
+ `(212) 555-1234 5`. This includes non-ASCII digits such as `5`.
43
+ - Without `set:`, a US or Canadian number with letters among its first ten
44
+ digits (`519 444 000 ext 123`) is now invalid, as it already was with `set:`.
45
+ - With `set:`, a stored extension no longer keeps a leading `,` or `;` or
46
+ surrounding spaces (including non-breaking ones): `212-555-1234, ext 5`
47
+ becomes `(212) 555-1234 ext 5`. An extension written with non-ASCII digits
48
+ (`x5`) is kept instead of being dropped. Other marked extensions (`x5`,
49
+ `ext. 99`, `(x5)`, `#5`, …) are stored as before.
50
+ ### Fixed
51
+
52
+ - The email validator still backtracked catastrophically on input with quote
53
+ characters, despite the 2.0.0 fix: `'a"' * 28 + "@"` (57 characters) took
54
+ about a minute to reject. The two length checks at the start of the regex
55
+ caused it; they're now plain length checks, and that input fails in well
56
+ under a millisecond.
57
+ - Email addresses with non-ASCII letters that case-fold to ASCII ones, such as
58
+ `a@ſx.com`, are rejected, like every other non-ASCII address.
59
+ - `country: :CA` (or `:ca`) now validates as Canada, as the README says. A
60
+ symbol was only ever treated as a method name, so without a `CA` method the
61
+ validator used the model's `country` method instead, or skipped validation
62
+ silently.
63
+ - The phone validator's `area_key:` (and `format_as_phone`'s area key) now
64
+ accepts Australian states in any case, as a string or symbol. `:vic` or
65
+ `"vic"` used to silently get the NSW/ACT area code (02).
66
+
67
+ ### Documentation
68
+
69
+ - The README's error-type example used `errors.added?(:first_name, :all_caps)`,
70
+ which is always false for mixed case errors because they carry an `item:`
71
+ option. It now uses `of_kind?`, and explains when `added?` needs `item:`.
72
+
73
+ ## 2.0.0 (2026-10-01)
74
+
75
+ ### Breaking changes
76
+
77
+ - **Ruby 3.4+ and activemodel 7.2+ are required.** The gem now declares its
78
+ activemodel dependency and loads without Rails.
79
+ - **Errors are added by type instead of as translated strings.** All four
80
+ validators call `errors.add(attribute, :invalid)` (mixed case:
81
+ `:all_caps` / `:all_lowercase`), so `errors.added?(:email, :invalid)` and
82
+ `errors.details` work, and ActiveModel does the message lookup.
83
+ - Existing `activerecord.errors.models.<model>.attributes.<attribute>.<type>`
84
+ translations keep working.
85
+ - Non-ActiveRecord models now look under `activemodel.*` instead of
86
+ `activerecord.*`, and fall back to Rails' "is invalid".
87
+ - A `message:` (or `all_caps:` / `all_lowercase:`) option now takes
88
+ precedence over translations, as it does for Rails' own validators.
89
+ - The bundled mixed case messages moved from `activerecord.errors.messages`
90
+ to `errors.messages`.
91
+ - **`ActiveModel::Validations::EmailValidator` (and `PhoneValidator`,
92
+ `PostalCodeValidator`, `MixedCaseValidator`) no longer exist.** Use
93
+ `ValidationKit::EmailValidator` and so on. `validates :email, email: true`
94
+ is unchanged, and an app's own validator with the same name now takes
95
+ precedence over the gem's.
96
+ - **`MixedCaseValidator::ALL_CAPS` / `ALL_LOWERCASE`** are now the symbols
97
+ `:all_caps` / `:all_lowercase` instead of `1` / `-1`.
98
+ - **Validation is stricter**, so some values that used to pass are now
99
+ invalid:
100
+ - postal codes of the wrong length (e.g. a 7-digit US ZIP code)
101
+ - email addresses containing a newline
102
+ - phone and postal input that's blank once cleaned up (e.g. `"n/a"`), even
103
+ with `allow_blank`
104
+ - **`format_as_phone` returns `nil`** for numbers it can't format, instead of
105
+ the raw digits.
106
+
107
+ ### Fixed
108
+
109
+ - The email regex no longer backtracks catastrophically: crafted input that
110
+ took over a minute to reject now fails in under a millisecond.
111
+ - Postal codes of the wrong length were accepted and then replaced with `nil`
112
+ by `set:`.
113
+ - With `allow_blank`, junk phone and postal input was accepted and replaced
114
+ with `""` or `nil`.
115
+ - US/CA numbers with a leading 1 (`1-212-555-1234`) were stored with a
116
+ trailing space.
117
+ - Phone extensions could be taken from the wrong part of the input when the
118
+ last four digits appeared earlier in it.
119
+ - Country codes in lowercase or as symbols (`"us"`, `:US`) silently skipped
120
+ validation.
121
+ - Integer values (e.g. a ZIP code from an integer column) raised instead of
122
+ validating.
123
+ - Mixed case errors were missing the attribute name and ignored the
124
+ `attribute_name:` option, and the bundled English and French messages were
125
+ never loaded.
126
+ - Values with no letters (`"123"`) or in scripts without case failed as "all
127
+ caps", and short accented names skipped the check.
128
+ - A stray `|` in the AU phone regex character classes.
129
+
130
+ ### Added
131
+
132
+ - UK postcode validation and formatting (`UK` or `GB`).
133
+ - `source_code_uri`, `bug_tracker_uri`, and `changelog_uri` gemspec metadata,
134
+ and a declared MIT license.
135
+
136
+ ### Documentation
137
+
138
+ - One `README.md` replaces the per-validator READMEs, with current usage,
139
+ options, and a translation key reference. Added `LICENSE` and this
140
+ changelog.
141
+
142
+ ### Internal
143
+
144
+ - Minitest suite, CI on Ruby 3.4/4.0 against activemodel 7.2/8.0/8.1,
145
+ rubocop, Dependabot, and a trusted publishing release workflow. The gem now
146
+ packages only `lib/`, `README.md`, `LICENSE`, and `CHANGELOG.md`.
147
+
148
+ ## 1.0.5
149
+
150
+ Last release before 2.0.0.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2008-2026 The validation_kit contributors (see README.md)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # validation_kit
2
+
3
+ ActiveModel validators for email addresses, phone numbers, postal codes, and
4
+ mixed-case text.
5
+
6
+ ## Installation
7
+
8
+ Add the gem to your Gemfile:
9
+
10
+ ```ruby
11
+ gem "validation_kit"
12
+ ```
13
+
14
+ The validators are available in any class that includes
15
+ `ActiveModel::Validations`, including ActiveRecord models. If your app defines
16
+ its own validator with the same name (e.g. `app/validators/email_validator.rb`),
17
+ yours is used instead.
18
+
19
+ ## Email
20
+
21
+ Validates that an attribute is an email address.
22
+
23
+ ```ruby
24
+ class Person < ActiveRecord::Base
25
+ validates :email, email: true
26
+ end
27
+
28
+ Person.new(email: "Bob").valid? # => false
29
+ Person.new(email: "joe@foobar.com").valid? # => true
30
+ ```
31
+
32
+ Addresses are limited to 254 characters, and 64 before the `@` (RFC 5321). To
33
+ check an address outside a model, use
34
+ `ValidationKit::EmailValidator.valid_address?("joe@foobar.com")`.
35
+
36
+ ## Mixed case
37
+
38
+ Validates that a string isn't all capitals or all lowercase. Values with fewer
39
+ than three letters that have a case are skipped, so initials, digits, and
40
+ scripts without case (e.g. Chinese) never fail.
41
+
42
+ ```ruby
43
+ class Person < ActiveRecord::Base
44
+ validates :first_name, mixed_case: true
45
+ end
46
+
47
+ Person.new(first_name: "BOB").valid? # => false, "First name cannot be in all caps"
48
+ Person.new(first_name: "bob").valid? # => false, "First name cannot be in all lowercase"
49
+ Person.new(first_name: "Bob").valid? # => true
50
+ ```
51
+
52
+ | Option | Description |
53
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | `attribute_name:` | Name for `%{item}` in a custom message, used when the attribute has no translation (a translation wins). It doesn’t change `full_messages`, which use the attribute’s translated name. |
55
+ | `all_caps:` | Custom message for all-caps values. |
56
+ | `all_lowercase:` | Custom message for all-lowercase values. |
57
+
58
+ ## Phone
59
+
60
+ Strict validation for phone numbers in the United States (`US`), Canada
61
+ (`CA`), and Australia (`AU`).
62
+
63
+ ```ruby
64
+ class Person < ActiveRecord::Base
65
+ validates :phone, phone: { country: "US", set: true }, allow_blank: true
66
+ end
67
+ ```
68
+
69
+ | Option | Description |
70
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
71
+ | `country:` | See [Specifying the country](#specifying-the-country). |
72
+ | `set:` | Reformat the stored value: `"1-212-555-1234 x5"` becomes `"(212) 555-1234 x5"`, `"0298765432"` becomes `"(02) 9876 5432"`. |
73
+ | `area_key:` | For Australian landlines entered without an area code, the state whose area code to add: `NSW`, `ACT`, `VIC`, `TAS`, `QLD`, `SA`, `NT` or `WA`, in any case, as a string or symbol (`"VIC"`, `:qld`). It's always a fixed state, not a method name. Leave it out (or blank) for NSW/ACT (`02`); any other value raises `ArgumentError`. |
74
+
75
+ A US or Canadian number may end with an extension, which needs a marker in
76
+ front of its digits: any letter (as in `x`, `ext` or `extension`), or `#`, `#`
77
+ or `№` (`"212-555-1234 ext 5"`). Digits right after the number with no marker,
78
+ as in `"212-555-12345"`, make it invalid.
79
+
80
+ ## Postal code
81
+
82
+ Strict validation for postal and ZIP codes in the United States (`US`), Canada
83
+ (`CA`), the United Kingdom (`UK` or `GB`), Australia (`AU`), and New Zealand
84
+ (`NZ`).
85
+
86
+ ```ruby
87
+ class Person < ActiveRecord::Base
88
+ validates :postal_code, postal_code: { country: "CA", set: true }, allow_blank: true
89
+ end
90
+ ```
91
+
92
+ | Option | Description |
93
+ | ---------- | ----------------------------------------------------------------------------------------------------------------------- |
94
+ | `country:` | See [Specifying the country](#specifying-the-country). |
95
+ | `set:` | Reformat the stored value: Canadian and UK codes get a space (`K1A 0B1`, `SW1A 1AA`), US ZIP+4 a hyphen (`10001-1234`). |
96
+
97
+ ## Specifying the country
98
+
99
+ The phone and postal code validators need a country. Country codes are
100
+ case-insensitive (`"CA"`, `"ca"`, or `:CA`), and validation is skipped when the
101
+ country isn't supported.
102
+
103
+ ```ruby
104
+ # A fixed country code, as a string or a two-letter symbol
105
+ validates :phone, phone: { country: "CA" }
106
+ validates :phone, phone: { country: :ca }
107
+
108
+ # The name of a method on your model that returns the country code
109
+ validates :phone, phone: { country: :billing_country }
110
+
111
+ # Neither: the validator calls your model's country method
112
+ validates :phone, phone: true
113
+ ```
114
+
115
+ A symbol is a method name if your model has that public method; otherwise a
116
+ two-letter symbol is a country code. Any other symbol (such as a misspelled
117
+ method name) raises `ArgumentError` when validating, rather than skipping
118
+ validation. Two things that check can't catch: a misspelled two-letter method
119
+ name is read as a country code (and skipped if that country isn't supported),
120
+ and with `allow_nil:` or `allow_blank:` it only runs once the attribute has a
121
+ value.
122
+
123
+ ## Error messages
124
+
125
+ Errors are added with standard types, so the usual ActiveModel tools work:
126
+
127
+ ```ruby
128
+ person.errors.of_kind?(:email, :invalid) # email, phone, postal code
129
+ person.errors.of_kind?(:first_name, :all_caps) # mixed case (also :all_lowercase)
130
+ ```
131
+
132
+ `of_kind?` checks the attribute and error type. `added?` also compares the
133
+ error's options, and mixed case errors carry an `item:` option (the
134
+ attribute's name, for messages that use `%{item}`; see
135
+ [Translation keys](#translation-keys) for how it's chosen), so for them
136
+ `added?` only matches if you pass the same `item:`.
137
+
138
+ Pass `message:` (or `all_caps:` / `all_lowercase:` for mixed case) for a custom
139
+ message, or translate it like any other Rails validation error:
140
+
141
+ ```yaml
142
+ en:
143
+ activerecord:
144
+ errors:
145
+ models:
146
+ person:
147
+ attributes:
148
+ email:
149
+ invalid: "doesn’t look like an email address"
150
+ first_name:
151
+ all_caps: "can’t be all capitals"
152
+ ```
153
+
154
+ Like Rails’ own messages, these leave out the attribute’s name, because
155
+ `full_messages` adds it: "First name can’t be all capitals". Use `activemodel`
156
+ instead of `activerecord` for classes that aren't ActiveRecord models. The gem includes English and French messages for the
157
+ mixed case errors.
158
+
159
+ ## Translation keys
160
+
161
+ Each validator adds an error of the type below. Rails looks up its message in
162
+ this order, using the first key it finds. These are the keys for an
163
+ ActiveRecord model `Person`; use `activemodel` instead of `activerecord` for
164
+ other ActiveModel classes:
165
+
166
+ 1. `activerecord.errors.models.person.attributes.<attribute>.<type>`
167
+ 2. `activerecord.errors.models.person.<type>`
168
+ 3. `activerecord.errors.messages.<type>`
169
+ 4. `errors.attributes.<attribute>.<type>`
170
+ 5. `errors.messages.<type>`
171
+
172
+ A `message:` option (or `all_caps:` / `all_lowercase:`) skips the lookup.
173
+
174
+ | Validator | Error type | Default message (en) | Default message (fr) |
175
+ | ------------- | ---------------- | ---------------------------- | ------------------------------------------ |
176
+ | `email` | `:invalid` | "is invalid" (from Rails) | from Rails' locale files (e.g. rails-i18n) |
177
+ | `phone` | `:invalid` | "is invalid" (from Rails) | from Rails' locale files (e.g. rails-i18n) |
178
+ | `postal_code` | `:invalid` | "is invalid" (from Rails) | from Rails' locale files (e.g. rails-i18n) |
179
+ | `mixed_case` | `:all_caps` | "cannot be in all caps" | "ne peut pas être en majuscules" |
180
+ | `mixed_case` | `:all_lowercase` | "cannot be in all lowercase" | "ne peut pas être en minuscules" |
181
+
182
+ Like Rails' own messages, the mixed case messages leave out the attribute's
183
+ name, which `full_messages` adds: "First name cannot be in all caps". A custom
184
+ message or translation can include it as `%{item}`: the attribute's
185
+ translation at `activerecord.attributes.person.<attribute>`, then the
186
+ `attribute_name:` option, then the humanized attribute name.
187
+
188
+ ## Credits
189
+
190
+ - **Email, mixed case, and postal code validators:** [Paul Schreiber](https://paulschreiber.com/), 2010–2011.
191
+ - **Phone validator:** written by Kristina Lim, copyright © 2008 Syndeo
192
+ Media. Paul Schreiber added US
193
+ and Canada support (September 2010) and extension support (November 2010).
194
+ It is named after, and was originally built on, Jerrod Blavos's
195
+ [validates_as_phone](https://code.google.com/archive/p/validates-as-phone/) plugin.
196
+ - **Gem:** co-authored by Wes Morgan.
197
+
198
+ ## License
199
+
200
+ Released under the MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ValidationKit
4
+ # The country: option shared by the phone and postal code validators.
5
+ module CountryOption
6
+ private
7
+
8
+ # The country code to validate against, or nil to skip validation:
9
+ # - a String is the code itself ("CA")
10
+ # - a Symbol is the name of a method on the record that returns the code
11
+ # (:billing_country); if there's no such method, a two-letter symbol is
12
+ # the code itself (:CA), and anything else is a mistake, so it raises
13
+ # rather than silently skipping validation
14
+ # - without the option, the record's country method, if it has one
15
+ def country_for(record)
16
+ option = options[:country]
17
+
18
+ case option
19
+ when String
20
+ option
21
+ when Symbol
22
+ if record.respond_to?(option)
23
+ record.public_send(option)
24
+ elsif option.match?(/\A[a-z]{2}\z/i)
25
+ option
26
+ else
27
+ raise ArgumentError, "country: #{option.inspect} is neither a public method on " \
28
+ "#{record.class.name || record.class.inspect} nor a two-letter country code"
29
+ end
30
+ else
31
+ record.country if record.respond_to?(:country)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,21 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ValidationKit
2
4
  class EmailValidator < ActiveModel::EachValidator
3
- # This hangs ruby if you have more than one validates_as_email rule in the same object
4
- # Commenting it out for now
5
- # Using the one found at <http://fightingforalostcause.net/misc/2006/compare-email-regex.php>
6
- # EMAIL_ADDRESS_RE = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i
5
+ # RFC 5321's length limits: the whole address, and the part before the @.
6
+ # They're checked in validate_each rather than as lookaheads at the start of
7
+ # EMAIL_ADDRESS_RE, where they backtracked exponentially on input like
8
+ # 'a"a"a"…@' (about a minute for 57 characters).
9
+ MAX_LENGTH = 254
10
+ MAX_LOCAL_PART_LENGTH = 64
11
+
12
+ # The address syntax only; validate_each also applies the length limits.
13
+ EMAIL_ADDRESS_RE = /\A(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))\z/i
7
14
 
8
- EMAIL_ADDRESS_RE = /^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/i
15
+ # Whether address is a valid email address: the syntax and the length
16
+ # limits. For use outside a model; EMAIL_ADDRESS_RE alone skips the limits.
17
+ def self.valid_address?(address)
18
+ address = address.to_s
19
+ # The syntax is ASCII-only, but the regex's /i matching would also let
20
+ # Unicode case-folded letters (ſ, the Kelvin sign) through as s and k.
21
+ return false unless address.ascii_only?
22
+ return false if address.length > MAX_LENGTH
23
+
24
+ # The domain can't contain an @, so the last one ends the local part.
25
+ address.rpartition("@").first.length <= MAX_LOCAL_PART_LENGTH && EMAIL_ADDRESS_RE.match?(address)
26
+ end
9
27
 
10
28
  def validate_each(record, attribute, value)
11
- unless EMAIL_ADDRESS_RE.match(value)
12
- model_name = record.class.to_s
13
- message = I18n.t("activerecord.errors.models.#{model_name.underscore}.attributes.#{attribute}.invalid",
14
- :default => [:"activerecord.errors.models.#{model_name.underscore}.invalid",
15
- options[:message],
16
- :'activerecord.errors.messages.invalid'])
17
- record.errors.add(attribute, message)
18
- end
29
+ return if self.class.valid_address?(value)
30
+
31
+ record.errors.add(attribute, :invalid, message: options[:message])
19
32
  end
20
33
  end
21
34
  end
@@ -1,6 +1,5 @@
1
1
  en:
2
- activerecord:
3
- errors:
4
- messages:
5
- all_caps: "%{item} cannot be in all caps"
6
- all_lowercase: "%{item} cannot be in all lowercase"
2
+ errors:
3
+ messages:
4
+ all_caps: "cannot be in all caps"
5
+ all_lowercase: "cannot be in all lowercase"
@@ -1,6 +1,5 @@
1
1
  fr:
2
- activerecord:
3
- errors:
4
- messages:
5
- all_caps: "%{item} ne peut pas être en majuscules"
6
- all_lowercase: "%{item} ne peut pas être en minuscules"
2
+ errors:
3
+ messages:
4
+ all_caps: "ne peut pas être en majuscules"
5
+ all_lowercase: "ne peut pas être en minuscules"
@@ -1,42 +1,43 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ValidationKit
2
4
  class MixedCaseValidator < ActiveModel::EachValidator
3
- ALL_CAPS = 1
4
- ALL_LOWERCASE = -1
5
+ # Error types, also used as the I18n keys and option names for custom messages.
6
+ ALL_CAPS = :all_caps
7
+ ALL_LOWERCASE = :all_lowercase
5
8
 
6
9
  def validate_each(record, attribute, value)
7
10
  return if value.nil?
8
- return if value.gsub(/\W/, "").size < 3 # skip very short words
9
- error = nil
10
-
11
- if (value.upcase == value)
12
- error = ALL_CAPS
13
- elsif (value.downcase == value)
14
- error = ALL_LOWERCASE
15
- end
16
11
 
17
- return if error.nil?
12
+ # Only letters that have a case count, so digits, punctuation, and
13
+ # scripts without case (e.g. Chinese) never trigger an error.
14
+ upper = value.to_s.scan(/\p{Lu}/).size
15
+ lower = value.to_s.scan(/\p{Ll}/).size
16
+ return if upper + lower < 3 # skip very short words
18
17
 
19
- model_name = record.class.to_s
18
+ error = if lower.zero?
19
+ ALL_CAPS
20
+ elsif upper.zero?
21
+ ALL_LOWERCASE
22
+ end
20
23
 
21
- item_name = I18n.t("activerecord.attributes.#{model_name.underscore}.#{attribute}",
22
- :default => nil) or options[:attribute_name] or attribute
24
+ return if error.nil?
23
25
 
24
- if error == ALL_CAPS
25
- message = I18n.t("activerecord.errors.models.#{model_name.underscore}.attributes.#{attribute}.all_caps",
26
- :item => item_name,
27
- :default => [:"activerecord.errors.models.#{model_name.underscore}.all_caps",
28
- options[:all_caps],
29
- :'activerecord.errors.messages.all_caps'])
30
- elsif error == ALL_LOWERCASE
31
- message = I18n.t("activerecord.errors.models.#{model_name.underscore}.attributes.#{attribute}.all_lowercase",
32
- :item => item_name,
33
- :default => [:"activerecord.errors.models.#{model_name.underscore}.all_lowercase",
34
- options[:all_lowercase],
35
- :'activerecord.errors.messages.all_lowercase'])
36
- end
26
+ # The bundled messages leave the name to full_messages, like Rails' own.
27
+ # item: is still passed for custom messages and translations that use
28
+ # %{item}.
29
+ record.errors.add(attribute, error, item: item_name(record, attribute), message: options[error])
30
+ end
37
31
 
38
- record.errors.add(attribute, message)
32
+ private
39
33
 
34
+ # A translated attribute name wins, then the :attribute_name option, then
35
+ # the humanized attribute name.
36
+ def item_name(record, attribute)
37
+ scope = [record.class.i18n_scope, :attributes, record.class.model_name.i18n_key]
38
+ I18n.t(attribute, scope:, default: nil) ||
39
+ options[:attribute_name] ||
40
+ record.class.human_attribute_name(attribute)
40
41
  end
41
42
  end
42
43
  end