validators 3.1.1 → 3.4.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +11 -1
  5. data/README.md +49 -29
  6. data/Rakefile +0 -1
  7. data/data/reserved_subdomains.txt +2830 -0
  8. data/lib/validators.rb +12 -4
  9. data/lib/validators/constants.rb +1 -1
  10. data/lib/validators/disposable_domains.rb +18 -0
  11. data/lib/validators/disposable_emails.rb +22 -0
  12. data/lib/validators/locale/en.yml +5 -2
  13. data/lib/validators/locale/pt-BR.yml +1 -0
  14. data/lib/validators/{reserved_hostnames.rb → reserved_subdomains.rb} +3 -5
  15. data/lib/validators/tld.rb +7 -5
  16. data/lib/validators/validates_cnpj_format_of.rb +1 -2
  17. data/lib/validators/validates_cpf_format_of.rb +1 -2
  18. data/lib/validators/validates_email_format_of.rb +19 -4
  19. data/lib/validators/validates_ssh_private_key.rb +1 -3
  20. data/lib/validators/validates_ssh_public_key.rb +1 -3
  21. data/lib/validators/validates_subdomain.rb +69 -0
  22. data/lib/validators/validates_username.rb +15 -0
  23. data/lib/validators/version.rb +1 -1
  24. data/test/test_helper.rb +17 -2
  25. data/test/validators/disposable_email_test.rb +18 -5
  26. data/test/validators/validates_cnpj_format_of_test.rb +3 -3
  27. data/test/validators/validates_cpf_format_of_test.rb +3 -3
  28. data/test/validators/validates_email_format_of_test.rb +27 -2
  29. data/test/validators/validates_ssh_private_key/common_test.rb +3 -3
  30. data/test/validators/validates_ssh_public_key_test.rb +3 -3
  31. data/test/validators/validates_subdomain_test.rb +75 -0
  32. data/test/validators/validates_url_format_of/with_tld_validation_test.rb +18 -0
  33. data/test/validators/validates_url_format_of/without_tld_validation_test.rb +18 -0
  34. data/test/validators/{validates_reserved_username_test.rb → validates_username_test.rb} +27 -4
  35. data/validators.gemspec +11 -1
  36. metadata +116 -22
  37. data/bin/sync-disposable-hostnames +0 -35
  38. data/bin/sync-tld +0 -17
  39. data/data/disposable.json +0 -60403
  40. data/data/reserved_hostnames.json +0 -2836
  41. data/data/tld.json +0 -1516
  42. data/lib/validators/disposable_hostnames.rb +0 -11
  43. data/lib/validators/validates_reserved_hostname.rb +0 -45
  44. data/lib/validators/validates_reserved_username.rb +0 -29
  45. data/test/validators/validates_reserved_hostname_test.rb +0 -40
@@ -7,8 +7,9 @@ module Validators
7
7
  require "validators/ip"
8
8
  require "validators/tld"
9
9
  require "validators/hostname"
10
- require "validators/disposable_hostnames"
11
- require "validators/reserved_hostnames"
10
+ require "validators/disposable_domains"
11
+ require "validators/disposable_emails"
12
+ require "validators/reserved_subdomains"
12
13
 
13
14
  require "validators/validates_datetime"
14
15
  require "validators/validates_ip_address"
@@ -20,8 +21,15 @@ module Validators
20
21
  require "validators/validates_ssh_private_key"
21
22
  require "validators/validates_ssh_public_key"
22
23
  require "validators/validates_hostname_format_of"
23
- require "validators/validates_reserved_hostname"
24
- require "validators/validates_reserved_username"
24
+ require "validators/validates_subdomain"
25
+ require "validators/validates_username"
25
26
 
26
27
  I18n.load_path += Dir[File.join(__dir__, "validators/locale/*.yml")]
28
+
29
+ def self.require_dependency!(dep)
30
+ require dep
31
+ rescue LoadError
32
+ raise "#{dep} is not part of the bundle. " \
33
+ "Add it to your project's Gemfile."
34
+ end
27
35
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Validators
4
4
  EMAIL_FORMAT = /\A[a-z0-9]+([-._][a-z0-9]+)*(\+[^@]+)?@[a-z0-9]+([.-][a-z0-9]+)*\.[a-z]{2,}\z/i.freeze
5
- MICROSOFT_EMAIL_FORMAT = /\A[\w][\w\d._-]*[\w\d_-]+(\+[\w\d]+)?@(hotmail|outlook).com\z/i.freeze
5
+ MICROSOFT_EMAIL_FORMAT = /\A[a-z0-9][a-z0-9._-]*[a-z0-9_-]+(\+[a-z0-9]+)?@(hotmail|outlook).com\z/i.freeze
6
6
 
7
7
  # Source: https://github.com/henrik/validates_url_format_of
8
8
  IPV4_PART = /\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]/.freeze # 0-255
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validators
4
+ class DisposableDomains
5
+ def self.all
6
+ @all ||=
7
+ begin
8
+ Validators.require_dependency! "root_domain"
9
+ Validators.require_dependency! "email_data"
10
+ EmailData.disposable_domains
11
+ end
12
+ end
13
+
14
+ def self.include?(domain)
15
+ all.include?(domain)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validators
4
+ class DisposableEmails
5
+ def self.all
6
+ @all ||=
7
+ begin
8
+ Validators.require_dependency! "root_domain"
9
+ Validators.require_dependency! "email_data"
10
+ EmailData.disposable_emails
11
+ end
12
+ end
13
+
14
+ def self.include?(email)
15
+ mailbox, domain = email.to_s.split("@")
16
+ mailbox = mailbox.to_s.gsub(".", "")
17
+ mailbox = mailbox.gsub(/\+(.+)?\Z/, "")
18
+
19
+ all.include?("#{mailbox}@#{domain}")
20
+ end
21
+ end
22
+ end
@@ -3,7 +3,8 @@ en:
3
3
  activemodel: &activemodel
4
4
  errors:
5
5
  messages:
6
- disposable_email: "is not allowed (high-bounce domain)"
6
+ disposable_email: "is not allowed (high-bounce email)"
7
+ disposable_domain: "is not allowed (high-bounce domain)"
7
8
  invalid_cnpj: "is not a valid CNPJ"
8
9
  invalid_cpf: "is not a valid CPF"
9
10
  invalid_date: "is not a valid date"
@@ -20,8 +21,10 @@ en:
20
21
  invalid_ssh_private_key_type: "must be a %{value} key"
21
22
  invalid_ssh_public_key: "is not a valid public SSH key"
22
23
  invalid_url: "is not a valid address"
23
- reserved_hostname: "%{value} is a reserved hostname"
24
+ reserved_subdomain: "%{value} is a reserved subdomain"
25
+ invalid_subdomain: "is invalid"
24
26
  reserved_username: "%{value} is a reserved username"
27
+ invalid_username: "is invalid"
25
28
 
26
29
  activerecord:
27
30
  <<: *activemodel
@@ -4,6 +4,7 @@ pt-BR:
4
4
  errors:
5
5
  messages:
6
6
  disposable_email: "não é permitido (e-mail temporário)"
7
+ disposable_domain: "não é permitido (e-mail temporário)"
7
8
  invalid_cnpj: "não é um CNPJ válido"
8
9
  invalid_cpf: "não é um CPF válido"
9
10
  invalid_date: "não é uma data válida"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Validators
4
- class ReservedHostnames
5
- FILE_PATH = File.expand_path("../../data/reserved_hostnames.json", __dir__)
4
+ class ReservedSubdomains
5
+ FILE_PATH = File.expand_path("../../data/reserved_subdomains.txt", __dir__)
6
6
 
7
7
  def self.reserved?(hostname, matchers = nil)
8
8
  matchers = parse_list(matchers) if matchers
@@ -11,9 +11,7 @@ module Validators
11
11
  end
12
12
 
13
13
  def self.all
14
- @all ||= JSON
15
- .parse(File.read(FILE_PATH))
16
- .map {|matcher| parse(matcher) }
14
+ @all ||= File.read(FILE_PATH).lines.map {|matcher| parse(matcher.chomp) }
17
15
  end
18
16
 
19
17
  def self.parse(matcher)
@@ -2,10 +2,12 @@
2
2
 
3
3
  module Validators
4
4
  class TLD
5
- FILE_PATH = File.expand_path("../../data/tld.json", __dir__)
6
-
7
5
  def self.all
8
- @all ||= JSON.parse(File.read(FILE_PATH))
6
+ @all ||=
7
+ begin
8
+ Validators.require_dependency! "email_data"
9
+ EmailData.tlds
10
+ end
9
11
  end
10
12
 
11
13
  def self.host_with_valid_tld?(host)
@@ -13,10 +15,10 @@ module Validators
13
15
 
14
16
  return false if host.split(".").size == 1
15
17
 
16
- valid?(host[/\.([^.]+)$/, 1].to_s.downcase)
18
+ include?(host[/\.([^.]+)$/, 1].to_s.downcase)
17
19
  end
18
20
 
19
- def self.valid?(tld)
21
+ def self.include?(tld)
20
22
  all.include?(tld)
21
23
  end
22
24
  end
@@ -25,10 +25,9 @@ module ActiveModel
25
25
  # end
26
26
  #
27
27
  def validates_cnpj_format_of(*attr_names)
28
+ Validators.require_dependency! "cpf_cnpj"
28
29
  require "cnpj"
29
30
  validates_with CnpjValidator, _merge_attributes(attr_names)
30
- rescue LoadError
31
- raise "cpf_cnpj is not part of the bundle. Add it to Gemfile."
32
31
  end
33
32
 
34
33
  alias_method :validates_cnpj, :validates_cnpj_format_of
@@ -25,10 +25,9 @@ module ActiveModel
25
25
  # end
26
26
  #
27
27
  def validates_cpf_format_of(*attr_names)
28
+ Validators.require_dependency! "cpf_cnpj"
28
29
  require "cpf"
29
30
  validates_with CpfValidator, _merge_attributes(attr_names)
30
- rescue LoadError
31
- raise "cpf_cnpj is not part of the bundle. Add it to Gemfile."
32
31
  end
33
32
 
34
33
  alias_method :validates_cpf, :validates_cpf_format_of
@@ -14,6 +14,7 @@ module ActiveModel
14
14
 
15
15
  validate_tld(record, attribute, value, options) if check_tld
16
16
  validate_email_format(record, attribute, value, options)
17
+ validate_disposable_domain(record, attribute, value, options) unless allow_disposable
17
18
  validate_disposable_email(record, attribute, value, options) unless allow_disposable
18
19
  end
19
20
 
@@ -41,12 +42,24 @@ module ActiveModel
41
42
  )
42
43
  end
43
44
 
44
- def validate_disposable_email(record, attribute, value, _options)
45
+ def validate_disposable_domain(record, attribute, value, _options)
46
+ return unless value
47
+
45
48
  hostname = value.to_s.split(AT_SIGN).last.to_s.downcase
49
+ root_domain = RootDomain.call(hostname)
50
+
51
+ return unless Validators::DisposableDomains.include?(root_domain)
52
+
53
+ record.errors.add(
54
+ attribute,
55
+ :disposable_domain,
56
+ value: value
57
+ )
58
+ end
46
59
 
47
- return if Validators::DisposableHostnames.all.none? do |disposable_hostname|
48
- hostname == disposable_hostname || hostname.end_with?(".#{disposable_hostname}")
49
- end
60
+ def validate_disposable_email(record, attribute, value, _options)
61
+ return unless value
62
+ return unless Validators::DisposableEmails.include?(value)
50
63
 
51
64
  record.errors.add(
52
65
  attribute,
@@ -64,6 +77,8 @@ module ActiveModel
64
77
  # end
65
78
  #
66
79
  def validates_email_format_of(*attr_names)
80
+ Validators.require_dependency! "root_domain"
81
+ Validators.require_dependency! "email_data"
67
82
  validates_with EmailValidator, _merge_attributes(attr_names)
68
83
  end
69
84
 
@@ -56,10 +56,8 @@ module ActiveModel
56
56
  # end
57
57
  #
58
58
  def validates_ssh_private_key(*attr_names)
59
- require "sshkey"
59
+ Validators.require_dependency! "sshkey"
60
60
  validates_with SshPrivateKeyValidator, _merge_attributes(attr_names)
61
- rescue LoadError
62
- raise "sshkey is not part of the bundle. Add it to Gemfile."
63
61
  end
64
62
  end
65
63
  end
@@ -25,10 +25,8 @@ module ActiveModel
25
25
  # end
26
26
  #
27
27
  def validates_ssh_public_key(*attr_names)
28
- require "sshkey"
28
+ Validators.require_dependency! "sshkey"
29
29
  validates_with SshPublicKeyValidator, _merge_attributes(attr_names)
30
- rescue LoadError
31
- raise "sshkey is not part of the bundle. Add it to Gemfile."
32
30
  end
33
31
  end
34
32
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Validations
5
+ class SubdomainValidator < EachValidator
6
+ def validate_each(record, attribute, value)
7
+ return if value.blank? && options[:allow_blank]
8
+ return if value.nil? && options[:allow_nil]
9
+
10
+ value = value.to_s
11
+
12
+ validate_reserved_subdomain(record, attribute, value)
13
+ validate_format(record, attribute, value)
14
+ end
15
+
16
+ def reserved?(subdomain)
17
+ ::Validators::ReservedSubdomains.reserved?(subdomain, options[:in])
18
+ end
19
+
20
+ def validate_reserved_subdomain(record, attribute, value)
21
+ return unless options.fetch(:reserved, true)
22
+ return unless reserved?(value)
23
+
24
+ record.errors.add(
25
+ attribute,
26
+ :"reserved_#{options[:error_name]}",
27
+ message: options[:message],
28
+ value: value
29
+ )
30
+ end
31
+
32
+ def validate_format(record, attribute, value)
33
+ return if Validators::Hostname.valid_label?(value)
34
+
35
+ record.errors.add(
36
+ attribute,
37
+ :"invalid_#{options[:error_name]}",
38
+ message: options[:message],
39
+ value: value
40
+ )
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+ # Validates whether or not the specified host label is valid.
46
+ # The `in: array` can have strings and patterns. A pattern is everything
47
+ # that starts with `/` and will be parsed as a regular expression.
48
+ #
49
+ # Notice that subdomains will be normalized; it'll be downcased and have
50
+ # its underscores and hyphens stripped before validating.
51
+ #
52
+ # class User < ActiveRecord::Base
53
+ # # Validates format and rejects reserved subdomains.
54
+ # validates_subdomain :subdomain
55
+ #
56
+ # # Validates against a custom list.
57
+ # validates_subdomain :subdomain, in: %w[www]
58
+ #
59
+ # # Rejects reserved domains validation.
60
+ # validates_subdomain :subdomain, reserved: false
61
+ # end
62
+ #
63
+ def validates_subdomain(*attr_names)
64
+ options = _merge_attributes(attr_names).merge(error_name: :subdomain)
65
+ validates_with SubdomainValidator, options
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Validations
5
+ class UsernameValidator < SubdomainValidator
6
+ end
7
+
8
+ module ClassMethods
9
+ def validates_username(*attr_names)
10
+ options = _merge_attributes(attr_names).merge(error_name: :username)
11
+ validates_with UsernameValidator, options
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,7 +3,7 @@
3
3
  module Validators
4
4
  module Version
5
5
  MAJOR = 3
6
- MINOR = 1
6
+ MINOR = 4
7
7
  PATCH = 1
8
8
  STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
9
9
  end
@@ -3,7 +3,6 @@
3
3
  $VERBOSE = nil
4
4
 
5
5
  require "simplecov"
6
- SimpleCov.start
7
6
 
8
7
  SimpleCov.start do
9
8
  add_filter "test/support"
@@ -17,9 +16,25 @@ require "active_support/all"
17
16
  require "minitest/utils"
18
17
  require "minitest/autorun"
19
18
 
19
+ def build_email_with_filter(email)
20
+ mailbox, domain = email.split("@")
21
+ "#{mailbox}+#{SecureRandom.hex(3)}@#{domain}"
22
+ end
23
+
24
+ def build_email_with_dots(email)
25
+ mailbox, domain = email.split("@")
26
+ new_mailbox = mailbox.chars.map {|char| [char, "."] }.flatten[0..-2].join
27
+
28
+ "#{new_mailbox}@#{domain}"
29
+ end
30
+
20
31
  Time.zone = "America/Sao_Paulo"
21
32
  TLDs = Validators::TLD.all.sample(10)
22
- DISPOSABLE_EMAILS = Validators::DisposableHostnames.all.sample(10)
33
+ DISPOSABLE_DOMAINS = Validators::DisposableDomains.all.sample(10)
34
+ DISPOSABLE_EMAILS = Validators::DisposableEmails.all +
35
+ Validators::DisposableEmails.all.sample(10).map {|email| build_email_with_filter(email) } +
36
+ Validators::DisposableEmails.all.sample(10).map {|email| build_email_with_dots(email) } +
37
+ Validators::DisposableEmails.all.sample(10).map {|email| build_email_with_filter(build_email_with_dots(email)) }
23
38
 
24
39
  Dir[File.join(__dir__, "support/**/*.rb")].sort.each {|f| require f }
25
40
 
@@ -3,18 +3,19 @@
3
3
  require "test_helper"
4
4
 
5
5
  class DisposableEmailTest < Minitest::Test
6
- DISPOSABLE_EMAILS.each do |domain|
7
- test "rejects disposable e-mail (#{domain})" do
6
+ DISPOSABLE_DOMAINS.each do |domain|
7
+ test "rejects disposable domain (#{domain})" do
8
8
  User.validates_email_format_of :email
9
9
 
10
10
  user = User.new(email: "user@#{domain}")
11
11
  user.valid?
12
12
 
13
- assert_includes user.errors[:email], I18n.t("activerecord.errors.messages.disposable_email")
13
+ assert_includes user.errors[:email],
14
+ I18n.t("activerecord.errors.messages.disposable_domain")
14
15
  end
15
16
  end
16
17
 
17
- DISPOSABLE_EMAILS.each do |domain|
18
+ DISPOSABLE_DOMAINS.each do |domain|
18
19
  test "rejects disposable e-mail with subdomain (custom.#{domain})" do
19
20
  User.validates_email_format_of :email
20
21
 
@@ -22,7 +23,7 @@ class DisposableEmailTest < Minitest::Test
22
23
  user.valid?
23
24
 
24
25
  assert_includes user.errors[:email],
25
- "is not allowed (high-bounce domain)"
26
+ I18n.t("activerecord.errors.messages.disposable_domain")
26
27
  end
27
28
  end
28
29
 
@@ -34,4 +35,16 @@ class DisposableEmailTest < Minitest::Test
34
35
 
35
36
  assert user.errors[:email].empty?
36
37
  end
38
+
39
+ DISPOSABLE_EMAILS.each do |email|
40
+ test "rejects disposable e-mail (#{email})" do
41
+ User.validates_email_format_of :email
42
+
43
+ user = User.new(email: email)
44
+ user.valid?
45
+
46
+ assert_includes user.errors[:email],
47
+ I18n.t("activerecord.errors.messages.disposable_email")
48
+ end
49
+ end
37
50
  end
@@ -15,10 +15,10 @@ class ValidatesCnpjFormatOfTest < Minitest::Test
15
15
  end
16
16
  end
17
17
 
18
- test "fails when gem is not available" do
19
- assert_raises do
18
+ test "fails when cpf_cnpj is not available" do
19
+ assert_raises(StandardError, /cpf_cnpj is not part of the bundle/) do
20
20
  Class.new do
21
- expects(:require).with("cnpj").raises(LoadError)
21
+ Validators.expects(:require).with("cpf_cnpj").raises(LoadError, "-- cpf_cnpj")
22
22
 
23
23
  include ActiveModel::Model
24
24
  validates_cnpj_format_of :document