|
7 | 7 | from abc import ABC, abstractmethod |
8 | 8 | from itertools import zip_longest |
9 | 9 | from operator import attrgetter |
| 10 | +from urllib.parse import urlparse |
10 | 11 |
|
11 | 12 | from marshmallow import types |
12 | 13 | from marshmallow.exceptions import ValidationError |
@@ -210,11 +211,32 @@ def __call__(self, value: str) -> str: |
210 | 211 | if "://" in value: |
211 | 212 | scheme = value.split("://")[0].lower() |
212 | 213 | if scheme not in self.schemes: |
213 | | - raise ValidationError(message) |
| 214 | + raise ValidationError( |
| 215 | + f"Invalid URL scheme '{scheme}'. " |
| 216 | + f"Allowed schemes are: {', '.join(self.schemes)}." |
| 217 | + ) |
214 | 218 |
|
215 | 219 | regex = self._regex(self.relative, self.absolute, self.require_tld) |
216 | 220 |
|
217 | 221 | if not regex.search(value): |
| 222 | + if self.require_tld: |
| 223 | + try: |
| 224 | + # Extract the netloc (hostname and port) |
| 225 | + parsed_url = urlparse(value) |
| 226 | + hostname = parsed_url.hostname |
| 227 | + except (ValueError, TypeError, AttributeError): |
| 228 | + hostname = None |
| 229 | + |
| 230 | + if hostname: |
| 231 | + # Check if hostname is an IP address |
| 232 | + is_ip = re.match(r"\d+\.\d+\.\d+\.\d+", hostname) |
| 233 | + # Check if hostname contains a dot (.) |
| 234 | + has_tld = "." in hostname |
| 235 | + if not is_ip and not has_tld: |
| 236 | + raise ValidationError( |
| 237 | + "URL must include a top-level domain (e.g., '.com', '.org')." |
| 238 | + ) |
| 239 | + # Default error message for other failures |
218 | 240 | raise ValidationError(message) |
219 | 241 |
|
220 | 242 | return value |
|
0 commit comments