domain_extractor 0.2.5 → 0.2.6
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/CHANGELOG.md +46 -4
- data/lib/domain_extractor/domain_validator.rb +130 -121
- data/lib/domain_extractor/version.rb +1 -1
- data/spec/domain_validator_spec.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b770e3c09383122b5cae3baa952127a0f616ee721c2a241f1facd9ddc42a4762
|
|
4
|
+
data.tar.gz: de6e3561bba3d457da8a4cd9aee88c5f6c76aedaf233c3bda4930cb8402b2871
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 342694e42f321dbea6b197a99909afba2fc4de4d13d01e6e92e66f54fa7d286c1abdfbc1713c56709783d1d05a840523c8f0b202c89528bdeca754eade68cf60
|
|
7
|
+
data.tar.gz: e23a61526b995375057f34b6a87053c9a26e1f6b6699a521332829921ecb28d1fa6fcf13802fdb9f79637a45200bc1ff98fa47fb8179671fbaed27500e9c16e9
|
data/CHANGELOG.md
CHANGED
|
@@ -5,24 +5,65 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.2.
|
|
8
|
+
## [0.2.6] - 2025-11-09
|
|
9
9
|
|
|
10
|
-
###
|
|
10
|
+
### Fixed - Rails Validator Registration
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
**CRITICAL FIX**: Moved `DomainValidator` class to the **top-level namespace** (from `DomainExtractor::DomainValidator`) to ensure Rails can properly autoload and find the validator.
|
|
13
13
|
|
|
14
|
-
####
|
|
14
|
+
#### The Problem
|
|
15
|
+
|
|
16
|
+
Version 0.2.5 defined the validator as `DomainExtractor::DomainValidator`, which caused Rails to fail with:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
ArgumentError: Unknown validator: 'DomainValidator'
|
|
20
|
+
NameError: uninitialized constant Website::DomainValidator
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This occurred because when using `validates :url, domain: { ... }`, Rails searches for `DomainValidator` in:
|
|
24
|
+
|
|
25
|
+
1. The model's namespace (e.g., `Website::DomainValidator`)
|
|
26
|
+
2. The top-level namespace (`::DomainValidator`)
|
|
27
|
+
3. ActiveModel::Validations namespace
|
|
28
|
+
|
|
29
|
+
It does **not** search module namespaces like `DomainExtractor::`.
|
|
30
|
+
|
|
31
|
+
#### The Solution
|
|
32
|
+
|
|
33
|
+
- Moved `DomainValidator` to top-level namespace where Rails can find it
|
|
34
|
+
- Added `DomainExtractor::DomainValidator` as an alias for backward compatibility
|
|
35
|
+
- All functionality remains identical; only the class location changed
|
|
36
|
+
|
|
37
|
+
#### Verification
|
|
38
|
+
|
|
39
|
+
- All 151 tests pass including 35 validator-specific tests
|
|
40
|
+
- RuboCop clean with zero offenses
|
|
41
|
+
- Verified in production Rails 8 application
|
|
42
|
+
- Confirmed working with `validates :url, domain: { validation: :root_or_custom_subdomain }`
|
|
43
|
+
|
|
44
|
+
## [0.2.5] - 2025-11-09 [YANKED]
|
|
45
|
+
|
|
46
|
+
**This version was yanked due to validator registration issue. Use 0.2.6 instead.**
|
|
47
|
+
|
|
48
|
+
### Added Rails Integration - Custom ActiveModel Validator (BROKEN)
|
|
49
|
+
|
|
50
|
+
Added a comprehensive custom ActiveModel validator for declarative URL and domain validation in Rails applications. However, the validator was incorrectly namespaced and did not work in Rails applications.
|
|
51
|
+
|
|
52
|
+
#### Features (Broken in 0.2.5)
|
|
15
53
|
|
|
16
54
|
**Validation Modes:**
|
|
55
|
+
|
|
17
56
|
- `:standard` - Validates any parseable URL (default mode)
|
|
18
57
|
- `:root_domain` - Only allows root domains without subdomains (e.g., `example.com` ✅, `shop.example.com` ❌)
|
|
19
58
|
- `:root_or_custom_subdomain` - Allows root or custom subdomains but excludes `www` subdomain (e.g., `example.com` ✅, `shop.example.com` ✅, `www.example.com` ❌)
|
|
20
59
|
|
|
21
60
|
**Protocol Options:**
|
|
61
|
+
|
|
22
62
|
- `use_protocol` (default: `true`) - Controls whether protocol (http/https) is required in the URL
|
|
23
63
|
- `use_https` (default: `true`) - Controls whether HTTPS is required (only relevant when `use_protocol` is true)
|
|
24
64
|
|
|
25
65
|
**Usage Examples:**
|
|
66
|
+
|
|
26
67
|
```ruby
|
|
27
68
|
# Standard validation - any valid URL
|
|
28
69
|
validates :url, domain: { validation: :standard }
|
|
@@ -77,6 +118,7 @@ validates :domain, domain: {
|
|
|
77
118
|
#### Use Cases
|
|
78
119
|
|
|
79
120
|
Perfect for Rails applications requiring:
|
|
121
|
+
|
|
80
122
|
- Multi-tenant custom domain validation
|
|
81
123
|
- Secure URL validation (HTTPS enforcement)
|
|
82
124
|
- Subdomain-based architecture validation
|
|
@@ -16,151 +16,160 @@ rescue LoadError
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
19
|
+
# DomainValidator is a custom ActiveModel validator for URL/domain validation.
|
|
20
|
+
#
|
|
21
|
+
# This validator is defined at the top level so Rails can find it when using:
|
|
22
|
+
# validates :url, domain: { validation: :standard }
|
|
23
|
+
#
|
|
24
|
+
# Validation modes:
|
|
25
|
+
# - :standard - Validates any valid URL using DomainExtractor.valid?
|
|
26
|
+
# - :root_domain - Only allows root domains (no subdomains) like https://mysite.com
|
|
27
|
+
# - :root_or_custom_subdomain - Allows root or custom subdomains but excludes 'www'
|
|
28
|
+
#
|
|
29
|
+
# Optional flags:
|
|
30
|
+
# - use_protocol (default: true) - Whether protocol (http/https) is required
|
|
31
|
+
# - use_https (default: true) - Whether https is required (only if use_protocol is true)
|
|
32
|
+
#
|
|
33
|
+
# @example Standard validation
|
|
34
|
+
# validates :url, domain: { validation: :standard }
|
|
35
|
+
#
|
|
36
|
+
# @example Root domain only, no protocol required
|
|
37
|
+
# validates :url, domain: { validation: :root_domain, use_protocol: false }
|
|
38
|
+
#
|
|
39
|
+
# @example Root or custom subdomain with https required
|
|
40
|
+
# validates :url, domain: { validation: :root_or_custom_subdomain, use_https: true }
|
|
41
|
+
class DomainValidator < ActiveModel::EachValidator
|
|
42
|
+
VALIDATION_MODES = %i[standard root_domain root_or_custom_subdomain].freeze
|
|
43
|
+
WWW_SUBDOMAIN = 'www'
|
|
44
|
+
|
|
45
|
+
def validate_each(record, attribute, value)
|
|
46
|
+
return if blank?(value)
|
|
47
|
+
|
|
48
|
+
validation_mode = extract_validation_mode
|
|
49
|
+
use_protocol = options.fetch(:use_protocol, true)
|
|
50
|
+
use_https = options.fetch(:use_https, true)
|
|
51
|
+
|
|
52
|
+
normalized_url = normalize_url(value, use_protocol, use_https)
|
|
53
|
+
|
|
54
|
+
return unless protocol_valid?(record, attribute, normalized_url, use_protocol, use_https)
|
|
55
|
+
|
|
56
|
+
parsed = parse_and_validate_url(record, attribute, normalized_url)
|
|
57
|
+
return unless parsed
|
|
58
|
+
|
|
59
|
+
apply_validation_mode(record, attribute, parsed, validation_mode)
|
|
60
|
+
end
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
end
|
|
62
|
+
private
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
# Extract and validate the validation mode option
|
|
65
|
+
def extract_validation_mode
|
|
66
|
+
validation_mode = options.fetch(:validation, :standard)
|
|
67
|
+
return validation_mode if VALIDATION_MODES.include?(validation_mode)
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return validation_mode if VALIDATION_MODES.include?(validation_mode)
|
|
69
|
+
raise ArgumentError, "Invalid validation mode: #{validation_mode}. " \
|
|
70
|
+
"Must be one of: #{VALIDATION_MODES.join(', ')}"
|
|
71
|
+
end
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
# Check protocol requirements
|
|
74
|
+
def protocol_valid?(record, attribute, url, use_protocol, use_https)
|
|
75
|
+
return true unless use_protocol
|
|
76
|
+
return true if valid_protocol?(url, use_https)
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
protocol = use_https ? 'https://' : 'http:// or https://'
|
|
79
|
+
record.errors.add(attribute, "must use #{protocol}")
|
|
80
|
+
false
|
|
81
|
+
end
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
# Parse URL and validate it's valid
|
|
84
|
+
def parse_and_validate_url(record, attribute, url)
|
|
85
|
+
parsed = DomainExtractor.parse(url)
|
|
86
|
+
return parsed if parsed.valid?
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return parsed if parsed.valid?
|
|
88
|
+
record.errors.add(attribute, 'is not a valid URL')
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
# Apply the validation mode rules
|
|
93
|
+
def apply_validation_mode(record, attribute, parsed, validation_mode)
|
|
94
|
+
case validation_mode
|
|
95
|
+
when :standard
|
|
96
|
+
# Already validated - any valid URL passes
|
|
87
97
|
nil
|
|
98
|
+
when :root_domain
|
|
99
|
+
validate_root_domain(record, attribute, parsed)
|
|
100
|
+
when :root_or_custom_subdomain
|
|
101
|
+
validate_root_or_custom_subdomain(record, attribute, parsed)
|
|
88
102
|
end
|
|
103
|
+
end
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
nil
|
|
96
|
-
when :root_domain
|
|
97
|
-
validate_root_domain(record, attribute, parsed)
|
|
98
|
-
when :root_or_custom_subdomain
|
|
99
|
-
validate_root_or_custom_subdomain(record, attribute, parsed)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Check if value is blank (nil, empty string, or whitespace-only)
|
|
104
|
-
def blank?(value)
|
|
105
|
-
value.nil? || (value.respond_to?(:empty?) && value.empty?) ||
|
|
106
|
-
(value.is_a?(String) && value.strip.empty?)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Normalize URL for validation based on protocol requirements
|
|
110
|
-
def normalize_url(url, use_protocol, use_https)
|
|
111
|
-
return url if blank?(url)
|
|
105
|
+
# Check if value is blank (nil, empty string, or whitespace-only)
|
|
106
|
+
def blank?(value)
|
|
107
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?) ||
|
|
108
|
+
(value.is_a?(String) && value.strip.empty?)
|
|
109
|
+
end
|
|
112
110
|
|
|
113
|
-
|
|
111
|
+
# Normalize URL for validation based on protocol requirements
|
|
112
|
+
def normalize_url(url, use_protocol, use_https)
|
|
113
|
+
return url if blank?(url)
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
url = url.gsub(%r{\A[A-Za-z][A-Za-z0-9+\-.]*://}, '') unless use_protocol
|
|
115
|
+
url = url.strip
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
scheme = use_https ? 'https://' : 'http://'
|
|
121
|
-
url = scheme + url
|
|
122
|
-
end
|
|
117
|
+
# If protocol is not required, strip any existing protocol
|
|
118
|
+
url = url.gsub(%r{\A[A-Za-z][A-Za-z0-9+\-.]*://}, '') unless use_protocol
|
|
123
119
|
|
|
124
|
-
|
|
120
|
+
# Add protocol if needed for parsing
|
|
121
|
+
unless url.match?(%r{\A[A-Za-z][A-Za-z0-9+\-.]*://})
|
|
122
|
+
scheme = use_https ? 'https://' : 'http://'
|
|
123
|
+
url = scheme + url
|
|
125
124
|
end
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
url
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Check if URL has valid protocol
|
|
130
|
+
def valid_protocol?(url, use_https)
|
|
131
|
+
return true unless url.match?(%r{\A[A-Za-z][A-Za-z0-9+\-.]*://})
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
end
|
|
133
|
+
if use_https
|
|
134
|
+
url.start_with?('https://')
|
|
135
|
+
else
|
|
136
|
+
url.start_with?('http://', 'https://')
|
|
136
137
|
end
|
|
138
|
+
end
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
# Validate that URL is a root domain (no subdomain)
|
|
141
|
+
def validate_root_domain(record, attribute, parsed)
|
|
142
|
+
return unless parsed.subdomain?
|
|
141
143
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
record.errors.add(attribute, 'must be a root domain (no subdomains allowed)')
|
|
145
|
+
end
|
|
144
146
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
# Validate that URL is either root domain or has custom subdomain (not 'www')
|
|
148
|
+
def validate_root_or_custom_subdomain(record, attribute, parsed)
|
|
149
|
+
return unless parsed.subdomain == WWW_SUBDOMAIN
|
|
148
150
|
|
|
149
|
-
|
|
150
|
-
end
|
|
151
|
+
record.errors.add(attribute, 'cannot use www subdomain')
|
|
151
152
|
end
|
|
152
153
|
end
|
|
153
154
|
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
155
|
+
# Also register in DomainExtractor namespace for backwards compatibility
|
|
156
|
+
module DomainExtractor
|
|
157
|
+
# DomainValidator is now defined at the top level for Rails autoloading.
|
|
158
|
+
# This constant provides a reference for explicit usage.
|
|
159
|
+
#
|
|
160
|
+
# Validation modes:
|
|
161
|
+
# - :standard - Validates any valid URL using DomainExtractor.valid?
|
|
162
|
+
# - :root_domain - Only allows root domains (no subdomains) like https://mysite.com
|
|
163
|
+
# - :root_or_custom_subdomain - Allows root or custom subdomains, but excludes 'www'
|
|
164
|
+
#
|
|
165
|
+
# Optional flags:
|
|
166
|
+
# - use_protocol (default: true) - Whether protocol (http/https) is required
|
|
167
|
+
# - use_https (default: true) - Whether https is required (only if use_protocol is true)
|
|
168
|
+
#
|
|
169
|
+
# @example Standard validation
|
|
170
|
+
# validates :url, domain: { validation: :standard }
|
|
171
|
+
#
|
|
172
|
+
# @example Root domain only, no protocol required
|
|
173
|
+
# validates :url, domain: { validation: :root_domain, use_protocol: false }
|
|
174
|
+
DomainValidator = ::DomainValidator
|
|
166
175
|
end
|