1- use std:: { path:: PathBuf , str:: FromStr } ;
1+ use std:: {
2+ fmt:: { Debug , Display } ,
3+ path:: PathBuf ,
4+ str:: FromStr ,
5+ } ;
26
37use clap:: { Args , ValueHint , value_parser} ;
48use semver:: Version ;
59use snafu:: { ResultExt , Snafu , ensure} ;
10+ use strum:: EnumDiscriminants ;
611use url:: Host ;
712
813use crate :: build:: {
@@ -39,14 +44,14 @@ pub struct BuildArguments {
3944 pub target_platform : TargetPlatform ,
4045
4146 /// Image registry used in image manifests, URIs, and tags.
47+ /// The format is host[:port].
4248 #[ arg(
4349 short, long,
44- default_value_t = Self :: default_registry( ) ,
45- value_parser = Host :: parse,
50+ default_value_t = HostPort :: localhost( ) ,
4651 value_hint = ValueHint :: Hostname ,
4752 help_heading = "Registry Options"
4853 ) ]
49- pub registry : Host ,
54+ pub registry : HostPort ,
5055
5156 /// The namespace within the given registry.
5257 #[ arg(
@@ -125,10 +130,6 @@ impl BuildArguments {
125130 TargetPlatform :: Linux ( Architecture :: Amd64 )
126131 }
127132
128- fn default_registry ( ) -> Host {
129- Host :: Domain ( String :: from ( "oci.stackable.tech" ) )
130- }
131-
132133 fn default_target_containerfile ( ) -> PathBuf {
133134 PathBuf :: from ( "Dockerfile" )
134135 }
@@ -149,3 +150,129 @@ pub fn parse_image_version(input: &str) -> Result<Version, ParseImageVersionErro
149150
150151 Ok ( version)
151152}
153+
154+ #[ derive( Debug , PartialEq , Snafu , EnumDiscriminants ) ]
155+ pub enum ParseHostPortError {
156+ #[ snafu( display( "unexpected empty input" ) ) ]
157+ EmptyInput ,
158+
159+ #[ snafu( display( "invalid format, expected host[:port]" ) ) ]
160+ InvalidFormat ,
161+
162+ #[ snafu( display( "failed to parse host" ) ) ]
163+ InvalidHost { source : url:: ParseError } ,
164+
165+ #[ snafu( display( "failed to parse port" ) ) ]
166+ InvalidPort { source : std:: num:: ParseIntError } ,
167+ }
168+
169+ #[ derive( Clone , Debug ) ]
170+ pub struct HostPort {
171+ pub host : Host ,
172+ pub port : Option < u16 > ,
173+ }
174+
175+ impl Display for HostPort {
176+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
177+ match self . port {
178+ Some ( port) => write ! ( f, "{host}:{port}" , host = self . host) ,
179+ None => Display :: fmt ( & self . host , f) ,
180+ }
181+ }
182+ }
183+
184+ impl FromStr for HostPort {
185+ type Err = ParseHostPortError ;
186+
187+ fn from_str ( input : & str ) -> Result < Self , Self :: Err > {
188+ ensure ! ( !input. is_empty( ) , EmptyInputSnafu ) ;
189+
190+ let parts: Vec < _ > = input. split ( ':' ) . collect ( ) ;
191+
192+ match parts[ ..] {
193+ [ host] => {
194+ let host = Host :: parse ( host) . context ( InvalidHostSnafu ) ?;
195+ Ok ( Self { host, port : None } )
196+ }
197+ [ host, port] => {
198+ let host = Host :: parse ( host) . context ( InvalidHostSnafu ) ?;
199+ let port = u16:: from_str ( port) . context ( InvalidPortSnafu ) ?;
200+
201+ Ok ( Self {
202+ host,
203+ port : Some ( port) ,
204+ } )
205+ }
206+ _ => InvalidFormatSnafu . fail ( ) ,
207+ }
208+ }
209+ }
210+
211+ impl HostPort {
212+ pub fn localhost ( ) -> Self {
213+ HostPort {
214+ host : Host :: Domain ( String :: from ( "localhost" ) ) ,
215+ port : None ,
216+ }
217+ }
218+ }
219+
220+ #[ cfg( test) ]
221+ mod tests {
222+ use rstest:: rstest;
223+ use strum:: IntoDiscriminant ;
224+ use url:: ParseError ;
225+
226+ use super :: * ;
227+
228+ enum Either < L , R > {
229+ Left ( L ) ,
230+ Right ( R ) ,
231+ }
232+
233+ impl < L , R > Either < L , R >
234+ where
235+ L : PartialEq ,
236+ R : PartialEq ,
237+ {
238+ fn is_either ( & self , left : & L , right : & R ) -> bool {
239+ match self {
240+ Either :: Left ( l) => l. eq ( left) ,
241+ Either :: Right ( r) => r. eq ( right) ,
242+ }
243+ }
244+ }
245+
246+ #[ rstest]
247+ #[ case( "registry.example.org:65535" ) ]
248+ #[ case( "registry.example.org:8080" ) ]
249+ #[ case( "registry.example.org" ) ]
250+ #[ case( "example.org:8080" ) ]
251+ #[ case( "localhost:8080" ) ]
252+ #[ case( "example.org" ) ]
253+ #[ case( "localhost" ) ]
254+ fn valid_host_port ( #[ case] input : & str ) {
255+ let host_port = HostPort :: from_str ( input) . expect ( "must parse" ) ;
256+ assert_eq ! ( host_port. to_string( ) , input) ;
257+ }
258+
259+ #[ rustfmt:: skip]
260+ #[ rstest]
261+ // We use the discriminants here, because ParseIntErrors cannot be constructed outside of std.
262+ // As such, it is impossible to fully qualify the error we expect in cases where port parsing
263+ // fails.
264+ #[ case( "localhost:65536" , Either :: Right ( ParseHostPortErrorDiscriminants :: InvalidPort ) ) ]
265+ #[ case( "localhost:" , Either :: Right ( ParseHostPortErrorDiscriminants :: InvalidPort ) ) ]
266+ // Other errors can be fully qualified.
267+ #[ case( "with space:" , Either :: Left ( ParseHostPortError :: InvalidHost { source: ParseError :: IdnaError } ) ) ]
268+ #[ case( "with space" , Either :: Left ( ParseHostPortError :: InvalidHost { source: ParseError :: IdnaError } ) ) ]
269+ #[ case( ":" , Either :: Left ( ParseHostPortError :: InvalidHost { source: ParseError :: EmptyHost } ) ) ]
270+ #[ case( "" , Either :: Left ( ParseHostPortError :: EmptyInput ) ) ]
271+ fn invalid_host_port (
272+ #[ case] input : & str ,
273+ #[ case] expected_error : Either < ParseHostPortError , ParseHostPortErrorDiscriminants > ,
274+ ) {
275+ let error = HostPort :: from_str ( input) . expect_err ( "must not parse" ) ;
276+ assert ! ( expected_error. is_either( & error, & error. discriminant( ) ) ) ;
277+ }
278+ }
0 commit comments