Skip to main content

reinhardt_core/validators/
serialization.rs

1//! Validator serialization support
2//!
3//! This module provides serializable representations of validator configurations,
4//! allowing validators to be saved to and loaded from configuration files.
5//!
6//! # Feature Flag
7//!
8//! This module requires the `serde` feature to be enabled.
9//!
10//! # Examples
11//!
12//! ```
13//! use reinhardt_core::validators::serialization::{ValidatorConfig, StringValidatorConfig};
14//!
15//! // Create a configuration
16//! let config = ValidatorConfig::String(StringValidatorConfig {
17//!     min_length: Some(3),
18//!     max_length: Some(50),
19//!     pattern: None,
20//!     pattern_message: None,
21//! });
22//!
23//! // Serialize to JSON
24//! let json = serde_json::to_string(&config).unwrap();
25//!
26//! // Deserialize from JSON
27//! let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
28//! ```
29
30use serde::{Deserialize, Serialize};
31
32/// Configuration for string validators
33#[derive(Debug, Clone, Serialize, Deserialize, Default)]
34pub struct StringValidatorConfig {
35	/// Minimum length (inclusive)
36	#[serde(skip_serializing_if = "Option::is_none")]
37	pub min_length: Option<usize>,
38
39	/// Maximum length (inclusive)
40	#[serde(skip_serializing_if = "Option::is_none")]
41	pub max_length: Option<usize>,
42
43	/// Regex pattern for validation
44	#[serde(skip_serializing_if = "Option::is_none")]
45	pub pattern: Option<String>,
46
47	/// Custom message for pattern validation failure
48	#[serde(skip_serializing_if = "Option::is_none")]
49	pub pattern_message: Option<String>,
50}
51
52/// Configuration for numeric validators
53#[derive(Debug, Clone, Serialize, Deserialize, Default)]
54pub struct NumericValidatorConfig {
55	/// Minimum value (inclusive)
56	#[serde(skip_serializing_if = "Option::is_none")]
57	pub min: Option<f64>,
58
59	/// Maximum value (inclusive)
60	#[serde(skip_serializing_if = "Option::is_none")]
61	pub max: Option<f64>,
62
63	/// Custom error message
64	#[serde(skip_serializing_if = "Option::is_none")]
65	pub message: Option<String>,
66}
67
68/// Configuration for email validator
69#[derive(Debug, Clone, Serialize, Deserialize, Default)]
70pub struct EmailValidatorConfig {
71	/// Minimum length
72	#[serde(skip_serializing_if = "Option::is_none")]
73	pub min_length: Option<usize>,
74
75	/// Maximum length
76	#[serde(skip_serializing_if = "Option::is_none")]
77	pub max_length: Option<usize>,
78
79	/// Custom error message
80	#[serde(skip_serializing_if = "Option::is_none")]
81	pub message: Option<String>,
82}
83
84/// Configuration for URL validator
85#[derive(Debug, Clone, Serialize, Deserialize, Default)]
86pub struct UrlValidatorConfig {
87	/// Custom error message
88	#[serde(skip_serializing_if = "Option::is_none")]
89	pub message: Option<String>,
90}
91
92/// Configuration for IP address validator
93#[derive(Debug, Clone, Serialize, Deserialize, Default)]
94pub struct IpAddressValidatorConfig {
95	/// Whether to allow IPv4 addresses
96	#[serde(default = "default_true")]
97	pub allow_ipv4: bool,
98
99	/// Whether to allow IPv6 addresses
100	#[serde(default = "default_true")]
101	pub allow_ipv6: bool,
102
103	/// Custom error message
104	#[serde(skip_serializing_if = "Option::is_none")]
105	pub message: Option<String>,
106}
107
108fn default_true() -> bool {
109	true
110}
111
112/// Configuration for phone number validator
113#[derive(Debug, Clone, Serialize, Deserialize, Default)]
114pub struct PhoneNumberValidatorConfig {
115	/// Allowed country codes (e.g., ["1", "81", "44"])
116	#[serde(skip_serializing_if = "Option::is_none")]
117	pub country_codes: Option<Vec<String>>,
118
119	/// Whether to allow extension numbers
120	#[serde(default)]
121	pub allow_extensions: bool,
122
123	/// Custom error message
124	#[serde(skip_serializing_if = "Option::is_none")]
125	pub message: Option<String>,
126}
127
128/// Color format for color validator
129#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
130#[serde(rename_all = "lowercase")]
131pub enum ColorFormatConfig {
132	/// Hex color format (#RGB, #RRGGBB, #RRGGBBAA)
133	Hex,
134	/// RGB color format (rgb(255, 255, 255))
135	Rgb,
136	/// RGBA color format (rgba(255, 255, 255, 1.0))
137	Rgba,
138	/// HSL color format (hsl(360, 100%, 50%))
139	Hsl,
140	/// HSLA color format (hsla(360, 100%, 50%, 1.0))
141	Hsla,
142	/// Any supported color format
143	Any,
144}
145
146/// Configuration for color validator
147#[derive(Debug, Clone, Serialize, Deserialize, Default)]
148pub struct ColorValidatorConfig {
149	/// Allowed color formats
150	#[serde(skip_serializing_if = "Option::is_none")]
151	pub allowed_formats: Option<Vec<ColorFormatConfig>>,
152
153	/// Custom error message
154	#[serde(skip_serializing_if = "Option::is_none")]
155	pub message: Option<String>,
156}
157
158/// Configuration for UUID validator
159#[derive(Debug, Clone, Serialize, Deserialize, Default)]
160pub struct UuidValidatorConfig {
161	/// Custom error message
162	#[serde(skip_serializing_if = "Option::is_none")]
163	pub message: Option<String>,
164}
165
166/// Configuration for slug validator
167#[derive(Debug, Clone, Serialize, Deserialize, Default)]
168pub struct SlugValidatorConfig {
169	/// Whether to allow unicode characters
170	#[serde(default)]
171	pub allow_unicode: bool,
172
173	/// Custom error message
174	#[serde(skip_serializing_if = "Option::is_none")]
175	pub message: Option<String>,
176}
177
178/// Configuration for date/time validators
179#[derive(Debug, Clone, Serialize, Deserialize, Default)]
180pub struct DateTimeValidatorConfig {
181	/// Format string (e.g., "%Y-%m-%d", "%H:%M:%S")
182	#[serde(skip_serializing_if = "Option::is_none")]
183	pub format: Option<String>,
184
185	/// Custom error message
186	#[serde(skip_serializing_if = "Option::is_none")]
187	pub message: Option<String>,
188}
189
190/// Configuration for file type validator
191#[derive(Debug, Clone, Serialize, Deserialize, Default)]
192pub struct FileTypeValidatorConfig {
193	/// Allowed file extensions (e.g., ["jpg", "png", "gif"])
194	#[serde(skip_serializing_if = "Option::is_none")]
195	pub allowed_extensions: Option<Vec<String>>,
196
197	/// Allowed MIME types (e.g., ["image/jpeg", "image/png"])
198	#[serde(skip_serializing_if = "Option::is_none")]
199	pub allowed_mime_types: Option<Vec<String>>,
200
201	/// Custom error message
202	#[serde(skip_serializing_if = "Option::is_none")]
203	pub message: Option<String>,
204}
205
206/// Configuration for file size validator
207#[derive(Debug, Clone, Serialize, Deserialize, Default)]
208pub struct FileSizeValidatorConfig {
209	/// Maximum file size in bytes
210	#[serde(skip_serializing_if = "Option::is_none")]
211	pub max_size: Option<u64>,
212
213	/// Minimum file size in bytes
214	#[serde(skip_serializing_if = "Option::is_none")]
215	pub min_size: Option<u64>,
216
217	/// Custom error message
218	#[serde(skip_serializing_if = "Option::is_none")]
219	pub message: Option<String>,
220}
221
222/// Configuration for image dimension validator
223#[derive(Debug, Clone, Serialize, Deserialize, Default)]
224pub struct ImageDimensionValidatorConfig {
225	/// Minimum width in pixels
226	#[serde(skip_serializing_if = "Option::is_none")]
227	pub min_width: Option<u32>,
228
229	/// Maximum width in pixels
230	#[serde(skip_serializing_if = "Option::is_none")]
231	pub max_width: Option<u32>,
232
233	/// Minimum height in pixels
234	#[serde(skip_serializing_if = "Option::is_none")]
235	pub min_height: Option<u32>,
236
237	/// Maximum height in pixels
238	#[serde(skip_serializing_if = "Option::is_none")]
239	pub max_height: Option<u32>,
240
241	/// Custom error message
242	#[serde(skip_serializing_if = "Option::is_none")]
243	pub message: Option<String>,
244}
245
246/// Unified validator configuration enum
247///
248/// This enum represents all supported validator types and their configurations.
249/// It can be serialized to and deserialized from JSON or other formats.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251#[serde(tag = "type", rename_all = "snake_case")]
252pub enum ValidatorConfig {
253	/// String length and pattern validation
254	String(StringValidatorConfig),
255
256	/// Numeric range validation
257	Numeric(NumericValidatorConfig),
258
259	/// Email format validation
260	Email(EmailValidatorConfig),
261
262	/// URL format validation
263	Url(UrlValidatorConfig),
264
265	/// IP address validation
266	IpAddress(IpAddressValidatorConfig),
267
268	/// Phone number validation
269	PhoneNumber(PhoneNumberValidatorConfig),
270
271	/// Color format validation
272	Color(ColorValidatorConfig),
273
274	/// UUID format validation
275	Uuid(UuidValidatorConfig),
276
277	/// Slug format validation
278	Slug(SlugValidatorConfig),
279
280	/// Date format validation
281	Date(DateTimeValidatorConfig),
282
283	/// Time format validation
284	Time(DateTimeValidatorConfig),
285
286	/// DateTime format validation
287	DateTime(DateTimeValidatorConfig),
288
289	/// File type validation
290	FileType(FileTypeValidatorConfig),
291
292	/// File size validation
293	FileSize(FileSizeValidatorConfig),
294
295	/// Image dimension validation
296	ImageDimension(ImageDimensionValidatorConfig),
297}
298
299impl ValidatorConfig {
300	/// Create a string validator configuration
301	pub fn string() -> StringValidatorConfig {
302		StringValidatorConfig::default()
303	}
304
305	/// Create a numeric validator configuration
306	pub fn numeric() -> NumericValidatorConfig {
307		NumericValidatorConfig::default()
308	}
309
310	/// Create an email validator configuration
311	pub fn email() -> EmailValidatorConfig {
312		EmailValidatorConfig::default()
313	}
314
315	/// Create a URL validator configuration
316	pub fn url() -> UrlValidatorConfig {
317		UrlValidatorConfig::default()
318	}
319
320	/// Create an IP address validator configuration
321	pub fn ip_address() -> IpAddressValidatorConfig {
322		IpAddressValidatorConfig {
323			allow_ipv4: true,
324			allow_ipv6: true,
325			message: None,
326		}
327	}
328
329	/// Create a phone number validator configuration
330	pub fn phone_number() -> PhoneNumberValidatorConfig {
331		PhoneNumberValidatorConfig::default()
332	}
333
334	/// Create a color validator configuration
335	pub fn color() -> ColorValidatorConfig {
336		ColorValidatorConfig::default()
337	}
338
339	/// Create a UUID validator configuration
340	pub fn uuid() -> UuidValidatorConfig {
341		UuidValidatorConfig::default()
342	}
343
344	/// Create a slug validator configuration
345	pub fn slug() -> SlugValidatorConfig {
346		SlugValidatorConfig::default()
347	}
348}
349
350/// Builder pattern for StringValidatorConfig
351impl StringValidatorConfig {
352	/// Set minimum length
353	pub fn min_length(mut self, len: usize) -> Self {
354		self.min_length = Some(len);
355		self
356	}
357
358	/// Set maximum length
359	pub fn max_length(mut self, len: usize) -> Self {
360		self.max_length = Some(len);
361		self
362	}
363
364	/// Set regex pattern
365	pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
366		self.pattern = Some(pattern.into());
367		self
368	}
369
370	/// Set custom pattern error message
371	pub fn pattern_message(mut self, message: impl Into<String>) -> Self {
372		self.pattern_message = Some(message.into());
373		self
374	}
375
376	/// Build into a ValidatorConfig
377	pub fn build(self) -> ValidatorConfig {
378		ValidatorConfig::String(self)
379	}
380}
381
382/// Builder pattern for NumericValidatorConfig
383impl NumericValidatorConfig {
384	/// Set minimum value
385	pub fn min(mut self, value: f64) -> Self {
386		self.min = Some(value);
387		self
388	}
389
390	/// Set maximum value
391	pub fn max(mut self, value: f64) -> Self {
392		self.max = Some(value);
393		self
394	}
395
396	/// Set custom error message
397	pub fn message(mut self, msg: impl Into<String>) -> Self {
398		self.message = Some(msg.into());
399		self
400	}
401
402	/// Build into a ValidatorConfig
403	pub fn build(self) -> ValidatorConfig {
404		ValidatorConfig::Numeric(self)
405	}
406}
407
408/// Builder pattern for EmailValidatorConfig
409impl EmailValidatorConfig {
410	/// Set minimum length
411	pub fn min_length(mut self, len: usize) -> Self {
412		self.min_length = Some(len);
413		self
414	}
415
416	/// Set maximum length
417	pub fn max_length(mut self, len: usize) -> Self {
418		self.max_length = Some(len);
419		self
420	}
421
422	/// Set custom error message
423	pub fn message(mut self, msg: impl Into<String>) -> Self {
424		self.message = Some(msg.into());
425		self
426	}
427
428	/// Build into a ValidatorConfig
429	pub fn build(self) -> ValidatorConfig {
430		ValidatorConfig::Email(self)
431	}
432}
433
434#[cfg(test)]
435mod tests {
436	use super::*;
437
438	#[test]
439	fn test_string_config_serialization() {
440		let config = ValidatorConfig::String(StringValidatorConfig {
441			min_length: Some(3),
442			max_length: Some(50),
443			pattern: Some(r"^[a-z]+$".into()),
444			pattern_message: Some("Only lowercase letters allowed".into()),
445		});
446
447		let json = serde_json::to_string(&config).unwrap();
448		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
449
450		match loaded {
451			ValidatorConfig::String(cfg) => {
452				assert_eq!(cfg.min_length, Some(3));
453				assert_eq!(cfg.max_length, Some(50));
454				assert_eq!(cfg.pattern, Some(r"^[a-z]+$".into()));
455			}
456			_ => panic!("Expected String config"),
457		}
458	}
459
460	#[test]
461	fn test_numeric_config_serialization() {
462		let config = ValidatorConfig::Numeric(NumericValidatorConfig {
463			min: Some(0.0),
464			max: Some(100.0),
465			message: Some("Value must be between 0 and 100".into()),
466		});
467
468		let json = serde_json::to_string(&config).unwrap();
469		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
470
471		match loaded {
472			ValidatorConfig::Numeric(cfg) => {
473				assert_eq!(cfg.min, Some(0.0));
474				assert_eq!(cfg.max, Some(100.0));
475			}
476			_ => panic!("Expected Numeric config"),
477		}
478	}
479
480	#[test]
481	fn test_email_config_serialization() {
482		let config = ValidatorConfig::Email(EmailValidatorConfig {
483			min_length: Some(5),
484			max_length: Some(254),
485			message: None,
486		});
487
488		let json = serde_json::to_string(&config).unwrap();
489		assert!(json.contains("\"type\":\"email\""));
490
491		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
492		match loaded {
493			ValidatorConfig::Email(cfg) => {
494				assert_eq!(cfg.min_length, Some(5));
495				assert_eq!(cfg.max_length, Some(254));
496			}
497			_ => panic!("Expected Email config"),
498		}
499	}
500
501	#[test]
502	fn test_ip_address_config_defaults() {
503		let json = r#"{"type":"ip_address"}"#;
504		let config: ValidatorConfig = serde_json::from_str(json).unwrap();
505
506		match config {
507			ValidatorConfig::IpAddress(cfg) => {
508				assert!(cfg.allow_ipv4);
509				assert!(cfg.allow_ipv6);
510			}
511			_ => panic!("Expected IpAddress config"),
512		}
513	}
514
515	#[test]
516	fn test_phone_number_config_serialization() {
517		let config = ValidatorConfig::PhoneNumber(PhoneNumberValidatorConfig {
518			country_codes: Some(vec!["1".into(), "81".into(), "44".into()]),
519			allow_extensions: true,
520			message: None,
521		});
522
523		let json = serde_json::to_string(&config).unwrap();
524		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
525
526		match loaded {
527			ValidatorConfig::PhoneNumber(cfg) => {
528				assert_eq!(cfg.country_codes.as_ref().unwrap().len(), 3);
529				assert!(cfg.allow_extensions);
530			}
531			_ => panic!("Expected PhoneNumber config"),
532		}
533	}
534
535	#[test]
536	fn test_color_config_serialization() {
537		let config = ValidatorConfig::Color(ColorValidatorConfig {
538			allowed_formats: Some(vec![ColorFormatConfig::Hex, ColorFormatConfig::Rgb]),
539			message: Some("Use hex or RGB format".into()),
540		});
541
542		let json = serde_json::to_string(&config).unwrap();
543		assert!(json.contains("\"hex\""));
544		assert!(json.contains("\"rgb\""));
545
546		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
547		match loaded {
548			ValidatorConfig::Color(cfg) => {
549				let formats = cfg.allowed_formats.unwrap();
550				assert_eq!(formats.len(), 2);
551				assert!(formats.contains(&ColorFormatConfig::Hex));
552				assert!(formats.contains(&ColorFormatConfig::Rgb));
553			}
554			_ => panic!("Expected Color config"),
555		}
556	}
557
558	#[test]
559	fn test_file_type_config_serialization() {
560		let config = ValidatorConfig::FileType(FileTypeValidatorConfig {
561			allowed_extensions: Some(vec!["jpg".into(), "png".into()]),
562			allowed_mime_types: Some(vec!["image/jpeg".into(), "image/png".into()]),
563			message: None,
564		});
565
566		let json = serde_json::to_string(&config).unwrap();
567		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
568
569		match loaded {
570			ValidatorConfig::FileType(cfg) => {
571				assert_eq!(cfg.allowed_extensions.as_ref().unwrap().len(), 2);
572				assert_eq!(cfg.allowed_mime_types.as_ref().unwrap().len(), 2);
573			}
574			_ => panic!("Expected FileType config"),
575		}
576	}
577
578	#[test]
579	fn test_image_dimension_config_serialization() {
580		let config = ValidatorConfig::ImageDimension(ImageDimensionValidatorConfig {
581			min_width: Some(100),
582			max_width: Some(1920),
583			min_height: Some(100),
584			max_height: Some(1080),
585			message: None,
586		});
587
588		let json = serde_json::to_string(&config).unwrap();
589		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
590
591		match loaded {
592			ValidatorConfig::ImageDimension(cfg) => {
593				assert_eq!(cfg.min_width, Some(100));
594				assert_eq!(cfg.max_width, Some(1920));
595				assert_eq!(cfg.min_height, Some(100));
596				assert_eq!(cfg.max_height, Some(1080));
597			}
598			_ => panic!("Expected ImageDimension config"),
599		}
600	}
601
602	#[test]
603	fn test_builder_pattern() {
604		let config = ValidatorConfig::string()
605			.min_length(1)
606			.max_length(100)
607			.pattern(r"^[a-zA-Z]+$")
608			.pattern_message("Only letters allowed")
609			.build();
610
611		match config {
612			ValidatorConfig::String(cfg) => {
613				assert_eq!(cfg.min_length, Some(1));
614				assert_eq!(cfg.max_length, Some(100));
615				assert!(cfg.pattern.is_some());
616				assert!(cfg.pattern_message.is_some());
617			}
618			_ => panic!("Expected String config"),
619		}
620	}
621
622	#[test]
623	fn test_numeric_builder() {
624		let config = ValidatorConfig::numeric()
625			.min(0.0)
626			.max(100.0)
627			.message("Value out of range")
628			.build();
629
630		match config {
631			ValidatorConfig::Numeric(cfg) => {
632				assert_eq!(cfg.min, Some(0.0));
633				assert_eq!(cfg.max, Some(100.0));
634				assert!(cfg.message.is_some());
635			}
636			_ => panic!("Expected Numeric config"),
637		}
638	}
639
640	#[test]
641	fn test_email_builder() {
642		let config = ValidatorConfig::email()
643			.min_length(5)
644			.max_length(254)
645			.message("Invalid email")
646			.build();
647
648		match config {
649			ValidatorConfig::Email(cfg) => {
650				assert_eq!(cfg.min_length, Some(5));
651				assert_eq!(cfg.max_length, Some(254));
652				assert!(cfg.message.is_some());
653			}
654			_ => panic!("Expected Email config"),
655		}
656	}
657
658	#[test]
659	fn test_skip_serializing_none_fields() {
660		let config = ValidatorConfig::String(StringValidatorConfig {
661			min_length: Some(1),
662			max_length: None,
663			pattern: None,
664			pattern_message: None,
665		});
666
667		let json = serde_json::to_string(&config).unwrap();
668
669		// None fields should not be in the JSON
670		assert!(json.contains("\"min_length\":1"));
671		assert!(!json.contains("\"max_length\""));
672		assert!(!json.contains("\"pattern\""));
673		assert!(!json.contains("\"pattern_message\""));
674	}
675
676	#[test]
677	fn test_deserialize_minimal_config() {
678		// Minimal config with only type and required fields
679		let json = r#"{"type":"string"}"#;
680		let config: ValidatorConfig = serde_json::from_str(json).unwrap();
681
682		match config {
683			ValidatorConfig::String(cfg) => {
684				assert!(cfg.min_length.is_none());
685				assert!(cfg.max_length.is_none());
686				assert!(cfg.pattern.is_none());
687			}
688			_ => panic!("Expected String config"),
689		}
690	}
691
692	#[test]
693	fn test_slug_config() {
694		let config = ValidatorConfig::Slug(SlugValidatorConfig {
695			allow_unicode: true,
696			message: Some("Invalid slug".into()),
697		});
698
699		let json = serde_json::to_string(&config).unwrap();
700		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
701
702		match loaded {
703			ValidatorConfig::Slug(cfg) => {
704				assert!(cfg.allow_unicode);
705				assert!(cfg.message.is_some());
706			}
707			_ => panic!("Expected Slug config"),
708		}
709	}
710
711	#[test]
712	fn test_uuid_config() {
713		let config = ValidatorConfig::Uuid(UuidValidatorConfig {
714			message: Some("Invalid UUID format".into()),
715		});
716
717		let json = serde_json::to_string(&config).unwrap();
718		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
719
720		match loaded {
721			ValidatorConfig::Uuid(cfg) => {
722				assert_eq!(cfg.message, Some("Invalid UUID format".into()));
723			}
724			_ => panic!("Expected UUID config"),
725		}
726	}
727
728	#[test]
729	fn test_file_size_config() {
730		let config = ValidatorConfig::FileSize(FileSizeValidatorConfig {
731			max_size: Some(10 * 1024 * 1024), // 10 MB
732			min_size: Some(1024),             // 1 KB
733			message: None,
734		});
735
736		let json = serde_json::to_string(&config).unwrap();
737		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
738
739		match loaded {
740			ValidatorConfig::FileSize(cfg) => {
741				assert_eq!(cfg.max_size, Some(10 * 1024 * 1024));
742				assert_eq!(cfg.min_size, Some(1024));
743			}
744			_ => panic!("Expected FileSize config"),
745		}
746	}
747
748	#[test]
749	fn test_url_config() {
750		let config = ValidatorConfig::Url(UrlValidatorConfig {
751			message: Some("Invalid URL".into()),
752		});
753
754		let json = serde_json::to_string(&config).unwrap();
755		assert!(json.contains("\"type\":\"url\""));
756
757		let loaded: ValidatorConfig = serde_json::from_str(&json).unwrap();
758		match loaded {
759			ValidatorConfig::Url(cfg) => {
760				assert!(cfg.message.is_some());
761			}
762			_ => panic!("Expected URL config"),
763		}
764	}
765}