1
- use std:: { fmt:: Display , ops:: Deref } ;
1
+ use std:: { fmt:: Display , net :: IpAddr , ops:: Deref , str :: FromStr } ;
2
2
3
3
use schemars:: JsonSchema ;
4
4
use serde:: { Deserialize , Serialize } ;
5
5
6
6
use crate :: validation;
7
7
8
- /// A validated hostname type, for use in CRDs.
9
- #[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , JsonSchema ) ]
8
+ /// A validated hostname type conforming to RFC 1123, e.g. not an IPv6 address.
9
+ #[ derive(
10
+ Serialize , Deserialize , Clone , Debug , Eq , PartialEq , Hash , Ord , PartialOrd , JsonSchema ,
11
+ ) ]
10
12
#[ serde( try_from = "String" , into = "String" ) ]
11
13
pub struct Hostname ( #[ validate( regex( path = "validation::RFC_1123_SUBDOMAIN_REGEX" ) ) ] String ) ;
12
14
15
+ impl FromStr for Hostname {
16
+ type Err = validation:: Errors ;
17
+
18
+ fn from_str ( value : & str ) -> Result < Self , Self :: Err > {
19
+ validation:: is_rfc_1123_subdomain ( & value) ?;
20
+ Ok ( Hostname ( value. to_owned ( ) ) )
21
+ }
22
+ }
23
+
13
24
impl TryFrom < String > for Hostname {
14
25
type Error = validation:: Errors ;
15
26
16
27
fn try_from ( value : String ) -> Result < Self , Self :: Error > {
17
- validation:: is_rfc_1123_subdomain ( & value) ?;
18
- Ok ( Hostname ( value) )
28
+ value. parse ( )
19
29
}
20
30
}
21
31
@@ -39,6 +49,68 @@ impl Deref for Hostname {
39
49
}
40
50
}
41
51
52
+ /// A validated host (either a [`Hostname`] or IP address) type.
53
+ #[ derive(
54
+ Serialize , Deserialize , Clone , Debug , Eq , PartialEq , Hash , Ord , PartialOrd , JsonSchema ,
55
+ ) ]
56
+ #[ serde( try_from = "String" , into = "String" ) ]
57
+ pub enum Host {
58
+ IpAddress ( IpAddr ) ,
59
+ Hostname ( Hostname ) ,
60
+ }
61
+
62
+ impl FromStr for Host {
63
+ type Err = validation:: Error ;
64
+
65
+ fn from_str ( value : & str ) -> Result < Self , Self :: Err > {
66
+ if let Ok ( ip) = value. parse :: < IpAddr > ( ) {
67
+ return Ok ( Host :: IpAddress ( ip) ) ;
68
+ }
69
+
70
+ if let Ok ( hostname) = value. parse ( ) {
71
+ return Ok ( Host :: Hostname ( hostname) ) ;
72
+ } ;
73
+
74
+ Err ( validation:: Error :: NotAHost { } )
75
+ }
76
+ }
77
+
78
+ impl TryFrom < String > for Host {
79
+ type Error = validation:: Error ;
80
+
81
+ fn try_from ( value : String ) -> Result < Self , Self :: Error > {
82
+ value. parse ( )
83
+ }
84
+ }
85
+
86
+ impl From < Host > for String {
87
+ fn from ( value : Host ) -> Self {
88
+ format ! ( "{value}" )
89
+ }
90
+ }
91
+
92
+ impl Display for Host {
93
+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
94
+ match self {
95
+ Host :: IpAddress ( ip) => write ! ( f, "{ip}" ) ,
96
+ Host :: Hostname ( hostname) => write ! ( f, "{hostname}" ) ,
97
+ }
98
+ }
99
+ }
100
+
101
+ impl Host {
102
+ /// Formats the host in such a way that it can be used in URLs.
103
+ pub fn as_url_host ( & self ) -> String {
104
+ match self {
105
+ Host :: IpAddress ( ip) => match ip {
106
+ IpAddr :: V4 ( ip) => ip. to_string ( ) ,
107
+ IpAddr :: V6 ( ip) => format ! ( "[{ip}]" ) ,
108
+ } ,
109
+ Host :: Hostname ( hostname) => hostname. to_string ( ) ,
110
+ }
111
+ }
112
+ }
113
+
42
114
/// A validated kerberos realm name type, for use in CRDs.
43
115
#[ derive( Serialize , Deserialize , Clone , Debug , PartialEq , JsonSchema ) ]
44
116
#[ serde( try_from = "String" , into = "String" ) ]
@@ -74,3 +146,48 @@ impl Deref for KerberosRealmName {
74
146
& self . 0
75
147
}
76
148
}
149
+
150
+ #[ cfg( test) ]
151
+ mod tests {
152
+ use super :: * ;
153
+
154
+ use rstest:: rstest;
155
+
156
+ #[ rstest]
157
+ #[ case( "foo" ) ]
158
+ #[ case( "foo.bar" ) ]
159
+ // Well this is also a valid hostname I guess
160
+ #[ case( "1.2.3.4" ) ]
161
+ fn test_host_and_hostname_parsing_success ( #[ case] hostname : String ) {
162
+ let parsed_hostname: Hostname = hostname. parse ( ) . expect ( "hostname can not be parsed" ) ;
163
+ // Every host is also a valid hostname
164
+ let parsed_host: Host = hostname. parse ( ) . expect ( "host can not be parsed" ) ;
165
+
166
+ // Also test the round-trip
167
+ assert_eq ! ( parsed_hostname. to_string( ) , hostname) ;
168
+ assert_eq ! ( parsed_host. to_string( ) , hostname) ;
169
+ }
170
+
171
+ #[ rstest]
172
+ #[ case( "" ) ]
173
+ #[ case( "foo.bar:1234" ) ]
174
+ #[ case( "fe80::1" ) ]
175
+ fn test_hostname_parsing_invalid_input ( #[ case] hostname : & str ) {
176
+ assert ! ( hostname. parse:: <Hostname >( ) . is_err( ) ) ;
177
+ }
178
+
179
+ #[ rstest]
180
+ #[ case( "foo" , "foo" ) ]
181
+ #[ case( "foo.bar" , "foo.bar" ) ]
182
+ #[ case( "1.2.3.4" , "1.2.3.4" ) ]
183
+ #[ case( "fe80::1" , "[fe80::1]" ) ]
184
+ fn test_host_parsing_success ( #[ case] host : & str , #[ case] expected_url_host : & str ) {
185
+ // Every host is also a valid hostname
186
+ let parsed_host: Host = host. parse ( ) . expect ( "host can not be parsed" ) ;
187
+
188
+ // Also test the round-trip
189
+ assert_eq ! ( parsed_host. to_string( ) , host) ;
190
+
191
+ assert_eq ! ( parsed_host. as_url_host( ) , expected_url_host) ;
192
+ }
193
+ }
0 commit comments