siderite_validation/types/
url.rs1use serde::{Deserialize, Serialize};
4
5use crate::context::ValidationContext;
6use crate::dump::Dump;
7use crate::error::FieldError;
8use crate::rules;
9use crate::schema::{Schema, SchemaObject, SchemaRegistry};
10use crate::validate::Validate;
11use serde_json::Value;
12
13#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(try_from = "String", into = "String")]
19pub struct Url(::url::Url);
20
21impl Url {
22 pub fn new(value: impl AsRef<str>) -> Result<Self, FieldError> {
24 rules::parse_url(value.as_ref()).map(Self)
25 }
26
27 pub fn as_str(&self) -> &str {
29 self.0.as_str()
30 }
31}
32
33crate::types::impl_wrapper!(Url => ::url::Url);
34
35impl TryFrom<String> for Url {
36 type Error = FieldError;
37
38 fn try_from(value: String) -> Result<Self, Self::Error> {
39 Self::new(value)
40 }
41}
42
43impl TryFrom<&str> for Url {
44 type Error = FieldError;
45
46 fn try_from(value: &str) -> Result<Self, Self::Error> {
47 Self::new(value)
48 }
49}
50
51impl From<Url> for String {
52 fn from(value: Url) -> Self {
53 value.0.to_string()
54 }
55}
56
57impl AsRef<str> for Url {
58 fn as_ref(&self) -> &str {
59 self.0.as_str()
60 }
61}
62
63impl Validate for Url {
64 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
65 if super::prepare_as_string(input, ctx) {
66 ctx.check(Self::new(input.as_str().unwrap_or_default()).map(|_| ()));
67 }
68 }
69}
70
71impl Schema for Url {
72 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
73 SchemaObject::of_type("string").with("format", "uri")
74 }
75}
76
77impl Dump for Url {}
78
79#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[serde(try_from = "String", into = "String")]
85pub struct HttpUrl(::url::Url);
86
87fn is_http_scheme(url: &::url::Url) -> bool {
88 matches!(url.scheme(), "http" | "https")
89}
90
91impl HttpUrl {
92 pub fn new(value: impl AsRef<str>) -> Result<Self, FieldError> {
94 let url = rules::parse_url(value.as_ref())?;
95 if !is_http_scheme(&url) {
96 return Err(FieldError::new(
97 "url_scheme",
98 "URL scheme must be http or https",
99 ));
100 }
101 Ok(Self(url))
102 }
103
104 pub fn as_str(&self) -> &str {
106 self.0.as_str()
107 }
108}
109
110crate::types::impl_wrapper!(HttpUrl => ::url::Url);
111
112impl TryFrom<String> for HttpUrl {
113 type Error = FieldError;
114
115 fn try_from(value: String) -> Result<Self, Self::Error> {
116 Self::new(value)
117 }
118}
119
120impl TryFrom<&str> for HttpUrl {
121 type Error = FieldError;
122
123 fn try_from(value: &str) -> Result<Self, Self::Error> {
124 Self::new(value)
125 }
126}
127
128impl From<HttpUrl> for String {
129 fn from(value: HttpUrl) -> Self {
130 value.0.to_string()
131 }
132}
133
134impl AsRef<str> for HttpUrl {
135 fn as_ref(&self) -> &str {
136 self.0.as_str()
137 }
138}
139
140impl Validate for HttpUrl {
141 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
142 if super::prepare_as_string(input, ctx) {
143 ctx.check(Self::new(input.as_str().unwrap_or_default()).map(|_| ()));
144 }
145 }
146}
147
148impl Schema for HttpUrl {
149 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
150 SchemaObject::of_type("string").with("format", "uri")
151 }
152}
153
154impl Dump for HttpUrl {}
155
156#[cfg(test)]
157mod tests {
158 #![allow(clippy::unwrap_used)]
159
160 use super::*;
161 use crate::context::ValidationContext;
162 use crate::dump::{Dump, DumpOptions};
163 use crate::schema::schema_for;
164 use crate::types::prepare_codes;
165 use serde_json::json;
166
167 #[test]
168 fn url_construction() {
169 let url = Url::new("https://example.com/x").unwrap();
170 assert_eq!(url.as_str(), "https://example.com/x");
171 assert_eq!(url.scheme(), "https");
172 assert!(Url::new("ftp://files.example.com").is_ok());
173 assert_eq!(Url::new("not a url").unwrap_err().code, "url_parsing");
174 assert_eq!(Url::new("example.com").unwrap_err().code, "url_parsing");
175 }
176
177 #[test]
178 fn http_url_rejects_other_schemes() {
179 assert!(HttpUrl::new("http://example.com").is_ok());
180 assert!(HttpUrl::new("https://example.com").is_ok());
181 assert_eq!(
182 HttpUrl::new("ftp://example.com").unwrap_err().code,
183 "url_scheme"
184 );
185 assert_eq!(HttpUrl::new("nope").unwrap_err().code, "url_parsing");
186 }
187
188 #[test]
189 fn url_serde_round_trip_and_reject() {
190 let url = Url::new("https://example.com").unwrap();
191 let encoded = serde_json::to_string(&url).unwrap();
192 assert_eq!(encoded, "\"https://example.com/\"");
193 let round: Url = serde_json::from_str(&encoded).unwrap();
194 assert_eq!(round.as_str(), "https://example.com/");
195 assert!(serde_json::from_str::<Url>("\"not a url\"").is_err());
196 assert!(serde_json::from_str::<HttpUrl>("\"ftp://example.com\"").is_err());
197 }
198
199 #[test]
200 fn url_prepare_lax_and_strict() {
201 let (out, codes) =
202 prepare_codes::<Url>(json!("https://example.com"), ValidationContext::new());
203 assert_eq!(codes, Vec::<String>::new());
204 assert_eq!(out, json!("https://example.com"));
205 assert_eq!(
206 prepare_codes::<Url>(json!(1), ValidationContext::new()).1,
207 ["string_type"]
208 );
209 assert_eq!(
210 prepare_codes::<Url>(json!(1), ValidationContext::new().with_strict(true)).1,
211 ["string_type"]
212 );
213 assert_eq!(
214 prepare_codes::<Url>(json!("nope"), ValidationContext::new()).1,
215 ["url_parsing"]
216 );
217 assert_eq!(
218 prepare_codes::<HttpUrl>(json!("ftp://x.com"), ValidationContext::new()).1,
219 ["url_scheme"]
220 );
221 }
222
223 #[test]
224 fn url_schema_dump_and_parse_value() {
225 assert_eq!(
226 schema_for::<Url>().0.into_value(),
227 json!({"type": "string", "format": "uri"})
228 );
229 assert_eq!(
230 schema_for::<HttpUrl>().0.into_value(),
231 json!({"type": "string", "format": "uri"})
232 );
233 let url = Url::new("https://example.com").unwrap();
234 assert_eq!(
235 url.dump(&DumpOptions::new()).unwrap(),
236 json!("https://example.com/")
237 );
238 let parsed =
239 crate::parse_value::<HttpUrl>(json!("https://example.com/a"), ValidationContext::new())
240 .unwrap();
241 assert_eq!(parsed.as_str(), "https://example.com/a");
242 let err = crate::parse_value::<Url>(json!("nope"), ValidationContext::new()).unwrap_err();
243 assert_eq!(err.errors[0].code, "url_parsing");
244 }
245}