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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08132eca3d279a11cf379a83f5288cbf1de6dfe50f62dce4592091c7dfd0195f'
4
- data.tar.gz: 22bb6ffd2c8b71271eb0c0a7a26faecfd1faad1b834b26ddbdea921712c8ebed
3
+ metadata.gz: b770e3c09383122b5cae3baa952127a0f616ee721c2a241f1facd9ddc42a4762
4
+ data.tar.gz: de6e3561bba3d457da8a4cd9aee88c5f6c76aedaf233c3bda4930cb8402b2871
5
5
  SHA512:
6
- metadata.gz: a93a94135442996433fb0bee204e78a9d07fb1da7628cad8bdc7c5e4fd8477c7dd28e63a9e3ed4e6b4784027f35fe9c8402c16c5ea4be639dc90f5ae78dd6c7a
7
- data.tar.gz: fcc7cd325ceda6d08a598cca43af970ea1867125722958ebe2f652042c02b322888ac7071b5f99fcce076b81444c4ff4b98a41625c00e9d8ccfd3dd143ce0675
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.5] - 2025-11-09
8
+ ## [0.2.6] - 2025-11-09
9
9
 
10
- ### Added Rails Integration - Custom ActiveModel Validator
10
+ ### Fixed - Rails Validator Registration
11
11
 
12
- Added a comprehensive custom ActiveModel validator for declarative URL and domain validation in Rails applications. The validator integrates seamlessly with Rails 6, 7, and 8.
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
- #### Features
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
- module DomainExtractor
20
- # DomainValidator is a custom ActiveModel validator for URL/domain validation.
21
- #
22
- # Validation modes:
23
- # - :standard - Validates any valid URL using DomainExtractor.valid?
24
- # - :root_domain - Only allows root domains (no subdomains) like https://mysite.com
25
- # - :root_or_custom_subdomain - Allows root or custom subdomains, but excludes 'www'
26
- #
27
- # Optional flags:
28
- # - use_protocol (default: true) - Whether protocol (http/https) is required
29
- # - use_https (default: true) - Whether https is required (only if use_protocol is true)
30
- #
31
- # @example Standard validation
32
- # validates :url, domain: { validation: :standard }
33
- #
34
- # @example Root domain only, no protocol required
35
- # validates :url, domain: { validation: :root_domain, use_protocol: false }
36
- #
37
- # @example Root or custom subdomain with https required
38
- # validates :url, domain: { validation: :root_or_custom_subdomain, use_https: true }
39
- class DomainValidator < ActiveModel::EachValidator
40
- VALIDATION_MODES = %i[standard root_domain root_or_custom_subdomain].freeze
41
- WWW_SUBDOMAIN = 'www'
42
-
43
- def validate_each(record, attribute, value)
44
- return if blank?(value)
45
-
46
- validation_mode = extract_validation_mode
47
- use_protocol = options.fetch(:use_protocol, true)
48
- use_https = options.fetch(:use_https, true)
49
-
50
- normalized_url = normalize_url(value, use_protocol, use_https)
51
-
52
- return unless protocol_valid?(record, attribute, normalized_url, use_protocol, use_https)
53
-
54
- parsed = parse_and_validate_url(record, attribute, normalized_url)
55
- return unless parsed
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
- apply_validation_mode(record, attribute, parsed, validation_mode)
58
- end
62
+ private
59
63
 
60
- private
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
- # Extract and validate the validation mode option
63
- def extract_validation_mode
64
- validation_mode = options.fetch(:validation, :standard)
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
- raise ArgumentError, "Invalid validation mode: #{validation_mode}. " \
68
- "Must be one of: #{VALIDATION_MODES.join(', ')}"
69
- end
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
- # Check protocol requirements
72
- def protocol_valid?(record, attribute, url, use_protocol, use_https)
73
- return true unless use_protocol
74
- return true if valid_protocol?(url, use_https)
78
+ protocol = use_https ? 'https://' : 'http:// or https://'
79
+ record.errors.add(attribute, "must use #{protocol}")
80
+ false
81
+ end
75
82
 
76
- protocol = use_https ? 'https://' : 'http:// or https://'
77
- record.errors.add(attribute, "must use #{protocol}")
78
- false
79
- end
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
- # Parse URL and validate it's valid
82
- def parse_and_validate_url(record, attribute, url)
83
- parsed = DomainExtractor.parse(url)
84
- return parsed if parsed.valid?
88
+ record.errors.add(attribute, 'is not a valid URL')
89
+ nil
90
+ end
85
91
 
86
- record.errors.add(attribute, 'is not a valid URL')
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
- # Apply the validation mode rules
91
- def apply_validation_mode(record, attribute, parsed, validation_mode)
92
- case validation_mode
93
- when :standard
94
- # Already validated - any valid URL passes
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
- url = url.strip
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
- # If protocol is not required, strip any existing protocol
116
- url = url.gsub(%r{\A[A-Za-z][A-Za-z0-9+\-.]*://}, '') unless use_protocol
115
+ url = url.strip
117
116
 
118
- # Add protocol if needed for parsing
119
- unless url.match?(%r{\A[A-Za-z][A-Za-z0-9+\-.]*://})
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
- url
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
- # Check if URL has valid protocol
128
- def valid_protocol?(url, use_https)
129
- return true unless url.match?(%r{\A[A-Za-z][A-Za-z0-9+\-.]*://})
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
- if use_https
132
- url.start_with?('https://')
133
- else
134
- url.start_with?('http://', 'https://')
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
- # Validate that URL is a root domain (no subdomain)
139
- def validate_root_domain(record, attribute, parsed)
140
- return unless parsed.subdomain?
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
- record.errors.add(attribute, 'must be a root domain (no subdomains allowed)')
143
- end
144
+ record.errors.add(attribute, 'must be a root domain (no subdomains allowed)')
145
+ end
144
146
 
145
- # Validate that URL is either root domain or has custom subdomain (not 'www')
146
- def validate_root_or_custom_subdomain(record, attribute, parsed)
147
- return unless parsed.subdomain == WWW_SUBDOMAIN
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
- record.errors.add(attribute, 'cannot use www subdomain')
150
- end
151
+ record.errors.add(attribute, 'cannot use www subdomain')
151
152
  end
152
153
  end
153
154
 
154
- # Register the validator with ActiveModel if it's available
155
- if defined?(ActiveModel::Validations)
156
- module ActiveModel
157
- module Validations
158
- # Enable usage via validates :url, domain: { validation: :standard }
159
- module HelperMethods
160
- def validates_domain(*attr_names)
161
- validates_with DomainExtractor::DomainValidator, _merge_attributes(attr_names)
162
- end
163
- end
164
- end
165
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DomainExtractor
4
- VERSION = '0.2.5'
4
+ VERSION = '0.2.6'
5
5
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- RSpec.describe DomainExtractor::DomainValidator do
5
+ RSpec.describe DomainValidator do
6
6
  # Mock record class for testing
7
7
  let(:record_class) do
8
8
  Class.new do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: domain_extractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenSite AI