siderite_validation/types/
ip.rs1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
4
5use serde::{Deserialize, Serialize};
6
7use crate::context::ValidationContext;
8use crate::dump::Dump;
9use crate::error::FieldError;
10use crate::rules;
11use crate::schema::{Schema, SchemaObject, SchemaRegistry};
12use crate::validate::Validate;
13use serde_json::Value;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(try_from = "String", into = "String")]
21pub struct IpAddress(IpAddr);
22
23impl IpAddress {
24 pub fn new(value: impl AsRef<str>) -> Result<Self, FieldError> {
26 rules::parse_ip(value.as_ref()).map(Self)
27 }
28}
29
30crate::types::impl_wrapper!(IpAddress => IpAddr);
31
32impl TryFrom<String> for IpAddress {
33 type Error = FieldError;
34
35 fn try_from(value: String) -> Result<Self, Self::Error> {
36 Self::new(value)
37 }
38}
39
40impl TryFrom<&str> for IpAddress {
41 type Error = FieldError;
42
43 fn try_from(value: &str) -> Result<Self, Self::Error> {
44 Self::new(value)
45 }
46}
47
48impl From<IpAddress> for String {
49 fn from(value: IpAddress) -> Self {
50 value.0.to_string()
51 }
52}
53
54impl Validate for IpAddress {
55 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
56 if super::prepare_as_string(input, ctx) {
57 ctx.check(Self::new(input.as_str().unwrap_or_default()).map(|_| ()));
58 }
59 }
60}
61
62impl Schema for IpAddress {
63 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
64 SchemaObject::default().with(
65 "anyOf",
66 Value::Array(vec![
67 SchemaObject::of_type("string")
68 .with("format", "ipv4")
69 .into_value(),
70 SchemaObject::of_type("string")
71 .with("format", "ipv6")
72 .into_value(),
73 ]),
74 )
75 }
76}
77
78impl Dump for IpAddress {}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
82#[serde(try_from = "String", into = "String")]
83pub struct Ipv4Address(Ipv4Addr);
84
85impl Ipv4Address {
86 pub fn new(value: impl AsRef<str>) -> Result<Self, FieldError> {
88 rules::parse_ipv4(value.as_ref()).map(Self)
89 }
90}
91
92crate::types::impl_wrapper!(Ipv4Address => Ipv4Addr);
93
94impl TryFrom<String> for Ipv4Address {
95 type Error = FieldError;
96
97 fn try_from(value: String) -> Result<Self, Self::Error> {
98 Self::new(value)
99 }
100}
101
102impl TryFrom<&str> for Ipv4Address {
103 type Error = FieldError;
104
105 fn try_from(value: &str) -> Result<Self, Self::Error> {
106 Self::new(value)
107 }
108}
109
110impl From<Ipv4Address> for String {
111 fn from(value: Ipv4Address) -> Self {
112 value.0.to_string()
113 }
114}
115
116impl Validate for Ipv4Address {
117 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
118 if super::prepare_as_string(input, ctx) {
119 ctx.check(Self::new(input.as_str().unwrap_or_default()).map(|_| ()));
120 }
121 }
122}
123
124impl Schema for Ipv4Address {
125 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
126 SchemaObject::of_type("string").with("format", "ipv4")
127 }
128}
129
130impl Dump for Ipv4Address {}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
134#[serde(try_from = "String", into = "String")]
135pub struct Ipv6Address(Ipv6Addr);
136
137impl Ipv6Address {
138 pub fn new(value: impl AsRef<str>) -> Result<Self, FieldError> {
140 rules::parse_ipv6(value.as_ref()).map(Self)
141 }
142}
143
144crate::types::impl_wrapper!(Ipv6Address => Ipv6Addr);
145
146impl TryFrom<String> for Ipv6Address {
147 type Error = FieldError;
148
149 fn try_from(value: String) -> Result<Self, Self::Error> {
150 Self::new(value)
151 }
152}
153
154impl TryFrom<&str> for Ipv6Address {
155 type Error = FieldError;
156
157 fn try_from(value: &str) -> Result<Self, Self::Error> {
158 Self::new(value)
159 }
160}
161
162impl From<Ipv6Address> for String {
163 fn from(value: Ipv6Address) -> Self {
164 value.0.to_string()
165 }
166}
167
168impl Validate for Ipv6Address {
169 fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
170 if super::prepare_as_string(input, ctx) {
171 ctx.check(Self::new(input.as_str().unwrap_or_default()).map(|_| ()));
172 }
173 }
174}
175
176impl Schema for Ipv6Address {
177 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
178 SchemaObject::of_type("string").with("format", "ipv6")
179 }
180}
181
182impl Dump for Ipv6Address {}
183
184#[cfg(test)]
185mod tests {
186 #![allow(clippy::unwrap_used)]
187
188 use super::*;
189 use crate::context::ValidationContext;
190 use crate::dump::{Dump, DumpOptions};
191 use crate::schema::schema_for;
192 use crate::types::prepare_codes;
193 use serde_json::json;
194
195 #[test]
196 fn ip_construction() {
197 assert_eq!(
198 IpAddress::new("127.0.0.1").unwrap().into_inner(),
199 IpAddr::from([127, 0, 0, 1])
200 );
201 assert!(IpAddress::new("::1").is_ok());
202 assert_eq!(IpAddress::new("nope").unwrap_err().code, "ip_any_parsing");
203 assert_eq!(Ipv4Address::new("::1").unwrap_err().code, "ip_v4_parsing");
204 assert_eq!(
205 Ipv6Address::new("127.0.0.1").unwrap_err().code,
206 "ip_v6_parsing"
207 );
208 assert!(Ipv4Address::new("192.168.0.1").is_ok());
209 assert!(Ipv6Address::new("2001:db8::1").is_ok());
210 }
211
212 #[test]
213 fn ip_serde_round_trip_and_reject() {
214 let ip = Ipv4Address::new("10.0.0.1").unwrap();
215 let encoded = serde_json::to_string(&ip).unwrap();
216 assert_eq!(encoded, "\"10.0.0.1\"");
217 let round: Ipv4Address = serde_json::from_str(&encoded).unwrap();
218 assert_eq!(round.into_inner(), Ipv4Addr::new(10, 0, 0, 1));
219 assert!(serde_json::from_str::<Ipv4Address>("\"::1\"").is_err());
220 assert!(serde_json::from_str::<IpAddress>("\"not-an-ip\"").is_err());
221 }
222
223 #[test]
224 fn ip_prepare_lax_and_strict() {
225 let (out, codes) = prepare_codes::<IpAddress>(json!("127.0.0.1"), ValidationContext::new());
226 assert!(codes.is_empty());
227 assert_eq!(out, json!("127.0.0.1"));
228 assert_eq!(
229 prepare_codes::<IpAddress>(json!(1), ValidationContext::new()).1,
230 ["string_type"]
231 );
232 assert_eq!(
233 prepare_codes::<IpAddress>(json!(1), ValidationContext::new().with_strict(true)).1,
234 ["string_type"]
235 );
236 assert_eq!(
237 prepare_codes::<Ipv6Address>(json!("127.0.0.1"), ValidationContext::new()).1,
238 ["ip_v6_parsing"]
239 );
240 }
241
242 #[test]
243 fn ip_schema_dump_and_parse_value() {
244 assert_eq!(
245 schema_for::<Ipv4Address>().0.into_value(),
246 json!({"type": "string", "format": "ipv4"})
247 );
248 assert_eq!(
249 schema_for::<Ipv6Address>().0.into_value(),
250 json!({"type": "string", "format": "ipv6"})
251 );
252 assert_eq!(
253 schema_for::<IpAddress>().0.into_value(),
254 json!({
255 "anyOf": [
256 {"type": "string", "format": "ipv4"},
257 {"type": "string", "format": "ipv6"}
258 ]
259 })
260 );
261 let ip = IpAddress::new("::1").unwrap();
262 assert_eq!(ip.dump(&DumpOptions::new()).unwrap(), json!("::1"));
263 let parsed =
264 crate::parse_value::<Ipv4Address>(json!("1.2.3.4"), ValidationContext::new()).unwrap();
265 assert_eq!(parsed.to_string(), "1.2.3.4");
266 let err =
267 crate::parse_value::<IpAddress>(json!("nope"), ValidationContext::new()).unwrap_err();
268 assert_eq!(err.errors[0].code, "ip_any_parsing");
269 }
270}