Skip to main content

siderite_validation/types/
ip.rs

1//! IP address newtypes wrapping [`std::net`] types.
2
3use 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/// An IPv4 or IPv6 address.
16///
17/// Pydantic `IPvAnyAddress` also accepts integers; this type accepts only
18/// address strings.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(try_from = "String", into = "String")]
21pub struct IpAddress(IpAddr);
22
23impl IpAddress {
24    /// Parse `value` as an IPv4 or IPv6 address.
25    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/// An IPv4 address.
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
82#[serde(try_from = "String", into = "String")]
83pub struct Ipv4Address(Ipv4Addr);
84
85impl Ipv4Address {
86    /// Parse `value` as an IPv4 address.
87    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/// An IPv6 address.
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
134#[serde(try_from = "String", into = "String")]
135pub struct Ipv6Address(Ipv6Addr);
136
137impl Ipv6Address {
138    /// Parse `value` as an IPv6 address.
139    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}