diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 000000000..e85e4d8c5 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,147 @@ +// Copyright 2013-2014 Simon Sapin. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Formatting utilities for URLs. +//! +//! These formatters can be used to coerce various URL parts into strings. +//! +//! You can use `.to_string()`, as the formatters implement `Show`. + +use std::fmt::{Show, Formatter, FormatError}; +use super::Url; + +/// Formatter and serializer for URL path data. +pub struct PathFormatter<'a, T> { + /// The path as a slice of string-like objects (String or &str). + pub path: &'a [T] +} + +impl<'a, T: Str + Show> Show for PathFormatter<'a, T> { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), FormatError> { + if self.path.is_empty() { + formatter.write(b"/") + } else { + for path_part in self.path.iter() { + try!("/".fmt(formatter)); + try!(path_part.fmt(formatter)); + } + Ok(()) + } + } +} + + +/// Formatter and serializer for URL username and password data. +pub struct UserInfoFormatter<'a> { + /// URL username as a string slice. + pub username: &'a str, + + /// URL password as an optional string slice. + /// + /// You can convert an `Option` with `.as_ref().map(|s| s.as_slice())`. + pub password: Option<&'a str> +} + +impl<'a> Show for UserInfoFormatter<'a> { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), FormatError> { + if !self.username.is_empty() || self.password.is_some() { + try!(formatter.write(self.username.as_bytes())); + match self.password { + None => (), + Some(password) => { + try!(formatter.write(b":")); + try!(formatter.write(password.as_bytes())); + } + } + try!(formatter.write(b"@")); + } + Ok(()) + } +} + + +/// Formatter for URLs which ignores the fragment field. +pub struct UrlNoFragmentFormatter<'a> { + pub url: &'a Url +} + +impl<'a> Show for UrlNoFragmentFormatter<'a> { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), FormatError> { + try!(formatter.write(self.url.scheme.as_bytes())); + try!(formatter.write(b":")); + try!(self.url.scheme_data.fmt(formatter)); + match self.url.query { + None => (), + Some(ref query) => { + try!(formatter.write(b"?")); + try!(formatter.write(query.as_bytes())); + } + } + Ok(()) + } +} + + +/// Formatting Tests +#[cfg(test)] +mod tests { + use super::super::Url; + use super::{PathFormatter, UserInfoFormatter}; + + #[test] + fn path_formatting() { + let data = [ + (vec![], "/"), + (vec![""], "/"), + (vec!["test", "path"], "/test/path"), + (vec!["test", "path", ""], "/test/path/") + ]; + for &(ref path, result) in data.iter() { + assert_eq!(PathFormatter { + path: path.as_slice() + }.to_string(), result.to_string()); + } + } + + #[test] + fn userinfo_formatting() { + // Test data as (username, password, result) tuples. + let data = [ + ("", None, ""), + ("", Some(""), ":@"), + ("", Some("password"), ":password@"), + ("username", None, "username@"), + ("username", Some(""), "username:@"), + ("username", Some("password"), "username:password@") + ]; + for &(username, password, result) in data.iter() { + assert_eq!(UserInfoFormatter { + username: username, + password: password + }.to_string(), result.to_string()); + } + } + + #[test] + fn relative_scheme_url_formatting() { + let data = [ + ("http://example.com/", "http://example.com/"), + ("http://addslash.com", "http://addslash.com/"), + ("http://@emptyuser.com/", "http://emptyuser.com/"), + ("http://:@emptypass.com/", "http://:@emptypass.com/"), + ("http://user@user.com/", "http://user@user.com/"), + ("http://user:pass@userpass.com/", "http://user:pass@userpass.com/"), + ("http://slashquery.com/path/?q=something", "http://slashquery.com/path/?q=something"), + ("http://noslashquery.com/path?q=something", "http://noslashquery.com/path?q=something") + ]; + for &(input, result) in data.iter() { + let url = Url::parse(input).unwrap(); + assert_eq!(url.to_string(), result.to_string()); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 467450c7f..1d223407d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -150,6 +150,7 @@ pub use percent_encoding::{ PASSWORD_ENCODE_SET, USERNAME_ENCODE_SET, FORM_URLENCODED_ENCODE_SET, EncodeSet, }; +use format::{PathFormatter, UserInfoFormatter, UrlNoFragmentFormatter}; mod host; mod parser; @@ -157,6 +158,7 @@ mod urlutils; pub mod percent_encoding; pub mod form_urlencoded; pub mod punycode; +pub mod format; #[cfg(test)] mod tests; @@ -583,6 +585,14 @@ impl Url { scheme_data.lossy_percent_decode_password()) } + /// Serialize the URL's username and password, if any. + /// + /// Format: ":@" + #[inline] + pub fn serialize_userinfo<'a>(&'a mut self) -> Option { + self.relative_scheme_data().map(|scheme_data| scheme_data.serialize_userinfo()) + } + /// If the URL is in a *relative scheme*, return its structured host. #[inline] pub fn host<'a>(&'a self) -> Option<&'a Host> { @@ -698,26 +708,6 @@ impl Show for Url { } } -struct UrlNoFragmentFormatter<'a> { - url: &'a Url -} - -impl<'a> Show for UrlNoFragmentFormatter<'a> { - fn fmt(&self, formatter: &mut Formatter) -> Result<(), FormatError> { - try!(formatter.write(self.url.scheme.as_bytes())); - try!(formatter.write(b":")); - try!(self.url.scheme_data.fmt(formatter)); - match self.url.query { - None => (), - Some(ref query) => { - try!(formatter.write(b"?")); - try!(formatter.write(query.as_bytes())); - } - } - Ok(()) - } -} - impl Show for SchemeData { fn fmt(&self, formatter: &mut Formatter) -> Result<(), FormatError> { @@ -800,50 +790,49 @@ impl RelativeSchemeData { /// The returned string starts with a "/" slash, and components are separated by slashes. /// A trailing slash represents an empty last component. pub fn serialize_path(&self) -> String { - PathFormatter { path: &self.path }.to_string() + PathFormatter { + path: self.path.as_slice() + }.to_string() } -} - -struct PathFormatter<'a> { - path: &'a Vec -} -impl<'a> Show for PathFormatter<'a> { - fn fmt(&self, formatter: &mut Formatter) -> Result<(), FormatError> { - if self.path.is_empty() { - formatter.write(b"/") - } else { - for path_part in self.path.iter() { - try!(formatter.write(b"/")); - try!(formatter.write(path_part.as_bytes())); - } - Ok(()) - } + /// Serialize the userinfo as a string. + /// + /// Format: ":@". + pub fn serialize_userinfo(&self) -> String { + UserInfoFormatter { + username: self.username.as_slice(), + password: self.password.as_ref().map(|s| s.as_slice()) + }.to_string() } } + impl Show for RelativeSchemeData { fn fmt(&self, formatter: &mut Formatter) -> Result<(), FormatError> { + // Write the scheme-trailing double slashes. try!(formatter.write(b"//")); - if !self.username.is_empty() || self.password.is_some() { - try!(formatter.write(self.username.as_bytes())); - match self.password { - None => (), - Some(ref password) => { - try!(formatter.write(b":")); - try!(formatter.write(password.as_bytes())); - } - } - try!(formatter.write(b"@")); - } + + // Write the user info. + try!(UserInfoFormatter { + username: self.username.as_slice(), + password: self.password.as_ref().map(|s| s.as_slice()) + }.fmt(formatter)); + + // Write the host. try!(self.host.fmt(formatter)); + + // Write the port. match self.port { Some(port) => { try!(write!(formatter, ":{}", port)); }, None => {} } - PathFormatter { path: &self.path }.fmt(formatter) + + // Write the path. + PathFormatter { + path: self.path.as_slice() + }.fmt(formatter) } } @@ -852,6 +841,7 @@ trait ToUrlPath { fn to_url_path(&self) -> Result, ()>; } + impl ToUrlPath for path::posix::Path { fn to_url_path(&self) -> Result, ()> { if !self.is_absolute() { @@ -861,6 +851,7 @@ impl ToUrlPath for path::posix::Path { } } + impl ToUrlPath for path::windows::Path { fn to_url_path(&self) -> Result, ()> { if !self.is_absolute() { @@ -885,6 +876,7 @@ trait FromUrlPath { fn from_url_path(path: &[String]) -> Result; } + impl FromUrlPath for path::posix::Path { fn from_url_path(path: &[String]) -> Result { if path.is_empty() { @@ -906,6 +898,7 @@ impl FromUrlPath for path::posix::Path { } } + impl FromUrlPath for path::windows::Path { fn from_url_path(path: &[String]) -> Result { if path.is_empty() {