1use crate::error::{Error, ErrorKind};
2use crate::{Meta, Validator};
3use tanzim_value::{Value, ValueType};
4
5#[derive(Debug, Clone, Default)]
7pub struct Url {
8 meta: Meta,
9 schemes: Vec<String>,
10 require_host: bool,
11}
12
13impl Url {
14 pub fn with_meta(mut self, meta: Meta) -> Self {
16 self.meta = meta;
17 self
18 }
19
20 pub fn new() -> Self {
21 Self::default()
22 }
23
24 pub fn schemes(mut self, schemes: impl IntoIterator<Item = impl Into<String>>) -> Self {
26 for scheme in schemes {
27 self.schemes.push(scheme.into());
28 }
29 self
30 }
31
32 pub fn require_host(mut self) -> Self {
34 self.require_host = true;
35 self
36 }
37}
38
39crate::impl_meta_methods!(Url);
40
41impl Validator for Url {
42 fn meta(&self) -> &Meta {
43 &self.meta
44 }
45
46 fn meta_mut(&mut self) -> &mut Meta {
47 &mut self.meta
48 }
49
50 fn check(&self, value: &mut Value) -> Result<(), Error> {
51 let text = match value {
52 Value::String(text) => text,
53 other => {
54 return Err(Error::new(ErrorKind::Type {
55 expected: ValueType::String,
56 found: other.type_name(),
57 }));
58 }
59 };
60
61 let parsed = match url::Url::parse(text) {
62 Ok(parsed) => parsed,
63 Err(_) => return Err(Error::new(ErrorKind::Format { expected: "url" })),
64 };
65
66 if !self.schemes.is_empty() {
67 let mut allowed = false;
68 for scheme in &self.schemes {
69 if scheme.eq_ignore_ascii_case(parsed.scheme()) {
70 allowed = true;
71 break;
72 }
73 }
74 if !allowed {
75 return Err(Error::new(ErrorKind::Format {
76 expected: "url with an allowed scheme",
77 }));
78 }
79 }
80
81 if self.require_host && parsed.host().is_none() {
82 return Err(Error::new(ErrorKind::Format {
83 expected: "url with a host",
84 }));
85 }
86
87 Ok(())
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn string(text: &str) -> Value {
96 Value::String(text.to_string())
97 }
98
99 #[test]
100 fn accepts_url() {
101 assert!(
102 Url::new()
103 .validate(&mut string("https://example.com/x"))
104 .is_ok()
105 );
106 assert!(Url::new().validate(&mut string("not a url")).is_err());
107 }
108
109 #[test]
110 fn restricts_scheme_and_host() {
111 let validator = Url::new().schemes(["https"]).require_host();
112 assert!(
113 validator
114 .validate(&mut string("https://example.com"))
115 .is_ok()
116 );
117 assert!(
118 validator
119 .validate(&mut string("http://example.com"))
120 .is_err()
121 );
122 assert!(validator.validate(&mut string("mailto:a@b.com")).is_err());
123 }
124}