can_has_validations 1.0.0 → 1.2.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: 98d343f6a4e3bded76cac6973926550938ba481bc9357585c3d83fb8427f48e1
4
- data.tar.gz: dc5648966508e9db4ff6972475f1df6b813fa9beb7bd5b83192eb5483ad09e08
3
+ metadata.gz: 2c82e1883d7ee1fa1dd3a098f4f9bfd716b63321cec4b2aa4ff873351144f9fc
4
+ data.tar.gz: b4ab341c9a388e8e3bdcd8f10fbaa4ca1c2b7ab5f6db345ae3f7d84b65fc26b5
5
5
  SHA512:
6
- metadata.gz: 4b6bf6510a992f70ffb418010bc05ae0f97ddb181c7d0482c93ff87756abac7024b8c3e9cf32caee7cfefac7a93bf60b0292475732ea9dc3a39a2009196f6893
7
- data.tar.gz: 4a157d72a5ec06a71e3e07a93e4f9fa009fedc7e637b4188d7a7aad2aadb3f55b612e94f3eb3a54dcd1581874ae646f001d00efa956e31e2eb65633b5551a276
6
+ metadata.gz: 23684cebc4fce328fa9d9333a48332f7070c354c7876b5f0ee878917e7fdd6b2ae1256b33b99e9ed086bc1a6a323c440c4746b93851e4734182d9783ff5e22a2
7
+ data.tar.gz: '0941308278c6c9ea1a7b9b0a4b157b28e83bbe061a52f3dee0ffc2c9961e54f742fa7698516c02f5c8909795fe9297ec4093f744b323bb57a17c920dabaf4035'
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2012-2019 thomas morgan
1
+ Copyright 2012-2021 thomas morgan
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -10,6 +10,7 @@ Validations provided:
10
10
  * Existence
11
11
  * Grandparent
12
12
  * Hostname
13
+ * IP address
13
14
  * Ordering
14
15
  * URL
15
16
  * Write Once
@@ -147,6 +148,9 @@ TLD, so as to not fail as ICANN continues to add TLDs.
147
148
  # allows '_abc.example.com'
148
149
  validates :domain, hostname: {allow_underscore: true}
149
150
 
151
+ # allows '4.0/25.3.2.1.example.com'
152
+ validates :domain, hostname: {allow_slash: true}
153
+
150
154
  # allows 'a.example.com', but not 'example.com'
151
155
  validates :domain, hostname: {segments: 3..100}
152
156
 
@@ -158,6 +162,34 @@ TLD, so as to not fail as ICANN continues to add TLDs.
158
162
  # use 4 or 6 for ipv4 or ipv6 only
159
163
 
160
164
 
165
+ ## IP address validator ##
166
+
167
+ Ensures an attribute is generally formatted as a IP or IP block.
168
+
169
+ # allows '1.2.3.4' or '::1'
170
+ validates :ip, ipaddr: true
171
+
172
+ # allows '1.2.3.0/24' or '2001:db8::/64'
173
+ validates :cidr, ipaddr: {allow_block: true}
174
+
175
+ # if an ip block, the attribute must be fully contained within an allowed block.
176
+ # allows '10.0.0.1' and '10.0.0.0/24', but not '10.0.0.0/15'
177
+ validates :private_ip, ipaddr: {
178
+ allow_block: true,
179
+ within: [IPAddr.new('10.0.0.0/16'), '127.0.0.1']
180
+ # allowed IPs and blocks may be IPAddrs or Strings
181
+ }
182
+
183
+ # the inverse of :within
184
+ validates :public_ip6, ipaddr: {without: ['fc00::/7']]}
185
+
186
+ # :within and :without may also be procs or method names
187
+ validates :ip, ipaddr: {
188
+ within: :some_method,
189
+ without: ->(record){ ... }
190
+ }
191
+
192
+
161
193
  ## Ordering validators ##
162
194
 
163
195
  Ensures two attribute values maintain a relative order to one another. This is
@@ -183,7 +215,7 @@ Always skips over nil values; use `:presence` to validate those.
183
215
 
184
216
  ## URL validator ##
185
217
 
186
- Ensure an attribute is generally formatted as a URL. If `addressable/uri` is
218
+ Ensures an attribute is generally formatted as a URL. If `addressable/uri` is
187
219
  already loaded, it will be used to parse IDN's. Additionally, allowed schemes
188
220
  can be specified; they default to ['http','https'].
189
221
 
@@ -205,8 +237,8 @@ can be specified; they default to ['http','https'].
205
237
 
206
238
  ## Write Once validator ##
207
239
 
208
- Ensure that once a value is written, it becomes readonly. There are two uses
209
- for this.
240
+ Ensures that once a value is written, it becomes readonly. There are a few
241
+ uses for this.
210
242
 
211
243
  The first is as an equivalent to `attr_readonly :user_id` except that it also
212
244
  produces a validation error instead of silently ignoring the change as
@@ -221,6 +253,10 @@ sorts.
221
253
 
222
254
  validates :user_id, allow_nil: true, write_once: true
223
255
 
256
+ The third use is to allow a nil value, and treat the nil also as write-once.
257
+
258
+ validates :source, write_once: {immutable_nil: true}
259
+
224
260
 
225
261
  ## Error messages
226
262
 
@@ -232,6 +268,9 @@ Default messages are as follows:
232
268
  messages:
233
269
  invalid_email: "is an invalid email"
234
270
  invalid_hostname: "is an invalid hostname"
271
+ invalid_ip: "is an invalid IP"
272
+ ip_not_allowed: "is not an allowed IP"
273
+ single_ip_required: "must be a single IP"
235
274
  invalid_url: "is an invalid URL"
236
275
  unchangeable: "cannot be changed"
237
276
  before: "must be before %{attribute2}"
@@ -1,6 +1,6 @@
1
1
  require 'active_model/validations'
2
2
 
3
- %w(array email existence grandparent hostname ordering url write_once).each do |validator|
3
+ %w(array email existence grandparent hostname ipaddr ordering url write_once).each do |validator|
4
4
  require "can_has_validations/validators/#{validator}_validator"
5
5
  end
6
6
 
@@ -3,8 +3,10 @@ en:
3
3
  messages:
4
4
  invalid_email: "is an invalid email"
5
5
  invalid_hostname: "is an invalid hostname"
6
+ invalid_ip: "is an invalid IP"
7
+ ip_not_allowed: "is not an allowed IP"
8
+ single_ip_required: "must be a single IP"
6
9
  invalid_url: "is an invalid URL"
7
10
  unchangeable: "cannot be changed"
8
11
  before: "must be before %{attribute2}"
9
12
  after: "must be after %{attribute2}"
10
-
@@ -70,8 +70,7 @@ module ActiveModel
70
70
  private
71
71
 
72
72
  def count_errors(record)
73
- # more efficient than calling record.errors.size
74
- record.errors.messages.sum{|key, val| val.blank? ? 0 : val.size }
73
+ record.errors.count
75
74
  end
76
75
 
77
76
  def _parse_validates_options(options)
@@ -1,15 +1,55 @@
1
1
  # Ensure an attribute is generally formatted as an email.
2
2
  # eg: validates :user_email, email: true
3
3
 
4
+ require_relative 'hostname_validator'
5
+
4
6
  module ActiveModel::Validations
5
7
  class EmailValidator < ActiveModel::EachValidator
6
8
 
7
- EMAIL_REGEXP = /\A([a-z0-9._+-]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
9
+ EMAIL_REGEXP = /\A([a-z0-9._+-]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
10
+ SEGMENT_REGEXP = /\A[a-z0-9+_-]+\z/i
11
+ LABEL_REGEXP = HostnameValidator::LABEL_REGEXP
12
+ FINAL_LABEL_REGEXP = HostnameValidator::FINAL_LABEL_REGEXP
8
13
 
9
14
  def validate_each(record, attribute, value)
10
- unless value =~ EMAIL_REGEXP
15
+ unless email_valid?(value)
11
16
  record.errors.add(attribute, :invalid_email, options.merge(value: value))
12
17
  end
13
18
  end
19
+
20
+ def email_valid?(value)
21
+ return unless value
22
+ recipient, domain = value.split('@', 2)
23
+ is_valid = true
24
+
25
+ recipient ||= ''
26
+ is_valid &&= recipient.length <= 255
27
+ is_valid &&= recipient !~ /\.\./
28
+ is_valid &&= !recipient.starts_with?('.')
29
+ is_valid &&= !recipient.ends_with?('.')
30
+ recipient.split('.').each do |segment|
31
+ is_valid &&= segment =~ SEGMENT_REGEXP
32
+ end
33
+
34
+ domain ||= ''
35
+ if defined?(Addressable::IDNA)
36
+ domain &&= Addressable::IDNA.to_ascii(domain)
37
+ end
38
+ labels = domain.split('.')
39
+ is_valid &&= domain.length <= 255
40
+ is_valid &&= domain !~ /\.\./
41
+ is_valid &&= labels.size.in? 2..100
42
+ labels.each_with_index do |label, idx|
43
+ is_valid &&= label.length <= 63
44
+ if idx+1==labels.size
45
+ is_valid &&= label =~ FINAL_LABEL_REGEXP
46
+ else
47
+ is_valid &&= label =~ LABEL_REGEXP
48
+ end
49
+ end
50
+
51
+ is_valid
52
+ end
53
+
14
54
  end
15
55
  end
@@ -16,7 +16,8 @@ module ActiveModel::Validations
16
16
  if cousin.nil?
17
17
  options[:allow_nil]
18
18
  else
19
- association.send(options[:parent]) == cousin.send(options[:parent])
19
+ association &&
20
+ association.send(options[:parent]) == cousin.send(options[:parent])
20
21
  end
21
22
  end
22
23
  unless all_match
@@ -18,6 +18,8 @@
18
18
  # allows '*.example.com'
19
19
  # validates :domain, hostname: {allow_underscore: true}
20
20
  # allows '_abc.example.com'
21
+ # validates :domain, hostname: {allow_slash: true}
22
+ # allows '4.0/25.3.2.1.example.com' # rfc2317
21
23
  # validates :domain, hostname: {segments: 3..100}
22
24
  # allows 'a.example.com', but not 'example.com'
23
25
  # validates :domain, hostname: {allow_ip: true} # or 4 or 6 for ipv4 or ipv6 only
@@ -30,9 +32,9 @@ require 'resolv'
30
32
  module ActiveModel::Validations
31
33
  class HostnameValidator < ActiveModel::EachValidator
32
34
 
33
- LABEL_REGEXP = /\A([a-zA-Z0-9_]([a-zA-Z0-9_-]+)?)?[a-zA-Z0-9]\z/
34
- FINAL_LABEL_REGEXP = /\A(xn--[a-zA-Z0-9]{2,}|[a-zA-Z]{2,})\z/
35
- RESERVED_OPTIONS = %i(allow_ip allow_underscore allow_wildcard)
35
+ LABEL_REGEXP = %r{\A([a-zA-Z0-9_]([a-zA-Z0-9_/-]+)?)?[a-zA-Z0-9]\z}
36
+ FINAL_LABEL_REGEXP = %r{\A(xn--[a-zA-Z0-9]{2,}|[a-zA-Z]{2,})\z}
37
+ RESERVED_OPTIONS = %i(allow_ip allow_slash allow_underscore allow_wildcard)
36
38
 
37
39
  def validate_each(record, attribute, value)
38
40
  case options[:allow_ip]
@@ -55,6 +57,7 @@ module ActiveModel::Validations
55
57
  is_valid &&= value.length <= 255
56
58
  is_valid &&= value !~ /\.\./
57
59
  is_valid &&= value !~ /_/ unless options[:allow_underscore]
60
+ is_valid &&= value !~ %r{/} unless options[:allow_slash]
58
61
  is_valid &&= labels.size.in? segments
59
62
  labels.each_with_index do |label, idx|
60
63
  is_valid &&= label.length <= 63
@@ -0,0 +1,91 @@
1
+ # Ensure an attribute is generally formatted as a IP or IP block.
2
+ # eg: validates :ip, ipaddr: true
3
+ # validates :cidr, ipaddr: {allow_block: true}
4
+ # validates :private_ip, ipaddr: {within: [IPAddr.new('10.0.0.0/8'), '127.0.0.1']}
5
+ # ip must be within any one of the provided ips/blocks
6
+ # if ip is block, it must be fully contained within any one of the provided blocks
7
+ # validates :public_ip6, ipaddr: {without: ['fc00::/7']]}
8
+ # ip must be outside all of the provided ips/blocks
9
+ # if ip is block, it must be fully outside all of the provided blocks
10
+
11
+ require 'ipaddr'
12
+
13
+ module ActiveModel::Validations
14
+ class IpaddrValidator < ActiveModel::EachValidator
15
+
16
+ def initialize(options)
17
+ options[:within] = normalize_within options[:within], :within
18
+ options[:without] = normalize_within options[:without], :without
19
+ super
20
+ end
21
+
22
+ def validate_each(record, attribute, value)
23
+ allowed_ips = resolve_array record, options[:within]
24
+ disallowed_ips = resolve_array record, options[:without]
25
+
26
+ ip = case value
27
+ when IPAddr
28
+ ip
29
+ when String
30
+ IPAddr.new(value) rescue nil
31
+ end
32
+ unless ip
33
+ record.errors.add(attribute, :invalid_ip, options.merge(value: value))
34
+ return
35
+ end
36
+
37
+ if !options[:allow_block] && (ip.ipv4? && ip.prefix!=32 or ip.ipv6? && ip.prefix!=128)
38
+ record.errors.add(attribute, :single_ip_required, options.merge(value: value))
39
+ end
40
+ if allowed_ips && allowed_ips.none?{|blk| blk.include? ip}
41
+ record.errors.add(attribute, :ip_not_allowed, options.merge(value: value))
42
+ elsif disallowed_ips && disallowed_ips.any?{|blk| blk.include? ip}
43
+ record.errors.add(attribute, :ip_not_allowed, options.merge(value: value))
44
+ end
45
+ end
46
+
47
+
48
+ private
49
+
50
+ def normalize_within(val, key)
51
+ if val.nil? || val.respond_to?(:call) || val.is_a?(Symbol)
52
+ val
53
+ else
54
+ Array(val).flatten.map do |i|
55
+ case i
56
+ when IPAddr
57
+ i
58
+ when String
59
+ IPAddr.new i
60
+ else
61
+ raise "Unexpected value for #{key.inspect} : #{i}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def resolve_array(record, val)
68
+ res = if val.respond_to?(:call)
69
+ val.call(record)
70
+ elsif val.is_a?(Symbol)
71
+ record.send(val)
72
+ else
73
+ val
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+
80
+ # tests for & fixes broken IPAddr <= 1.2.2
81
+ if IPAddr.new('192.168.2.0/32').include? '192.168.2.0/24'
82
+ # warn 'IPAddr <= 1.2.2 is broken; monkey-patching'
83
+ class IPAddr
84
+ def include?(other)
85
+ range = to_range
86
+ other = coerce_other(other).to_range
87
+ range.begin <= other.begin && range.end >= other.end
88
+ end
89
+ alias === include?
90
+ end
91
+ end
@@ -12,14 +12,20 @@ module ActiveModel::Validations
12
12
  # nil to be allowed. prevent this.
13
13
  def validate(record)
14
14
  attributes.each do |attribute|
15
- value = record.read_attribute_for_validation(attribute)
16
- validate_each(record, attribute, value)
15
+ validate_each(record, attribute, nil)
17
16
  end
18
17
  end
19
18
 
20
- def validate_each(record, attribute, value)
21
- if record.persisted? && record.send("#{attribute}_changed?")
22
- if options[:immutable_nil] || !record.send("#{attribute}_was").nil?
19
+ def validate_each(record, attribute, _)
20
+ return unless record.persisted?
21
+ if !record.respond_to?("#{attribute}_changed?") && record.respond_to?("#{attribute}_id_changed?")
22
+ attr2 = "#{attribute}_id"
23
+ else
24
+ attr2 = attribute
25
+ end
26
+ if record.send("#{attr2}_changed?")
27
+ if options[:immutable_nil] || !record.send("#{attr2}_was").nil?
28
+ value = record.read_attribute_for_validation(attribute)
23
29
  record.errors.add(attribute, :unchangeable, options.except(:immutable_nil).merge!(value: value))
24
30
  end
25
31
  end
@@ -1,3 +1,3 @@
1
1
  module CanHasValidations
2
- VERSION = '1.0.0'
2
+ VERSION = '1.2.1'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: can_has_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - thomas morgan
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-16 00:00:00.000000000 Z
11
+ date: 2021-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '5.0'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6.1'
22
+ version: '6.2'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: '5.0'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6.1'
32
+ version: '6.2'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: sqlite3
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -44,7 +44,8 @@ dependencies:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
46
  version: '0'
47
- description: Assorted Rails 5.x-6.x validators.
47
+ description: 'Assorted Rails 5.x-6.x validators: Array, Email, Existence, Grandparent,
48
+ Hostname, IP address, Ordering, URL, Write Once'
48
49
  email:
49
50
  - tm@iprog.com
50
51
  executables: []
@@ -61,6 +62,7 @@ files:
61
62
  - lib/can_has_validations/validators/existence_validator.rb
62
63
  - lib/can_has_validations/validators/grandparent_validator.rb
63
64
  - lib/can_has_validations/validators/hostname_validator.rb
65
+ - lib/can_has_validations/validators/ipaddr_validator.rb
64
66
  - lib/can_has_validations/validators/ordering_validator.rb
65
67
  - lib/can_has_validations/validators/url_validator.rb
66
68
  - lib/can_has_validations/validators/write_once_validator.rb
@@ -98,9 +100,10 @@ files:
98
100
  - test/dummy/script/rails
99
101
  - test/test_helper.rb
100
102
  homepage: https://github.com/zarqman/can_has_validations
101
- licenses: []
103
+ licenses:
104
+ - MIT
102
105
  metadata: {}
103
- post_install_message:
106
+ post_install_message:
104
107
  rdoc_options: []
105
108
  require_paths:
106
109
  - lib
@@ -115,8 +118,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
118
  - !ruby/object:Gem::Version
116
119
  version: '0'
117
120
  requirements: []
118
- rubygems_version: 3.0.3
119
- signing_key:
121
+ rubygems_version: 3.0.9
122
+ signing_key:
120
123
  specification_version: 4
121
124
  summary: Assorted Rails 5.x-6.x validators
122
125
  test_files: