#![no_std] mod error; pub use error::Error; /// A parsed URL to extract different parts of the URL. pub struct Url<'a> { host: &'a str, scheme: UrlScheme, port: Option, path: &'a str, } #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub enum UrlScheme { /// HTTP scheme HTTP, /// HTTPS (HTTP + TLS) scheme HTTPS, /// MQTT scheme MQTT, /// MQTTS (MQTT + TLS) scheme MQTTS, } impl UrlScheme { /// Get the default port for scheme pub const fn default_port(&self) -> u16 { match self { UrlScheme::HTTP => 80, UrlScheme::HTTPS => 443, UrlScheme::MQTT => 1883, UrlScheme::MQTTS => 8883, } } } impl<'a> Url<'a> { /// Parse the provided url pub fn parse(url: &'a str) -> Result, Error> { let mut parts = url.split("://"); let scheme = parts.next().unwrap(); let host_port_path = parts.next().ok_or(Error::NoScheme)?; let scheme = if scheme.eq_ignore_ascii_case("http") { Ok(UrlScheme::HTTP) } else if scheme.eq_ignore_ascii_case("https") { Ok(UrlScheme::HTTPS) } else { Err(Error::UnsupportedScheme) }?; let (host, port, path) = if let Some(port_delim) = host_port_path.find(':') { // Port is defined let host = &host_port_path[..port_delim]; let rest = &host_port_path[port_delim..]; let (port, path) = if let Some(path_delim) = rest.find('/') { let port = rest[1..path_delim].parse::().ok(); let path = &rest[path_delim..]; let path = if path.is_empty() { "/" } else { path }; (port, path) } else { let port = rest[1..].parse::().ok(); (port, "/") }; (host, port, path) } else { let (host, path) = if let Some(needle) = host_port_path.find('/') { let host = &host_port_path[..needle]; let path = &host_port_path[needle..]; (host, if path.is_empty() { "/" } else { path }) } else { (host_port_path, "/") }; (host, None, path) }; Ok(Self { scheme, host, path, port, }) } /// Get the url scheme pub fn scheme(&self) -> UrlScheme { self.scheme } /// Get the url host pub fn host(&self) -> &'a str { self.host } /// Get the url port if specified pub fn port(&self) -> Option { self.port } /// Get the url port or the default port for the scheme pub fn port_or_default(&self) -> u16 { self.port.unwrap_or_else(|| self.scheme.default_port()) } /// Get the url path pub fn path(&self) -> &'a str { self.path } } #[cfg(test)] mod tests { extern crate std; use super::*; #[test] fn test_parse_no_scheme() { assert_eq!(Error::NoScheme, Url::parse("").err().unwrap()); assert_eq!(Error::NoScheme, Url::parse("http:/").err().unwrap()); } #[test] fn test_parse_unsupported_scheme() { assert_eq!( Error::UnsupportedScheme, Url::parse("something://").err().unwrap() ); } #[test] fn test_parse_no_host() { let url = Url::parse("http://").unwrap(); assert_eq!(url.scheme(), UrlScheme::HTTP); assert_eq!(url.host(), ""); assert_eq!(url.port_or_default(), 80); assert_eq!(url.path(), "/"); } #[test] fn test_parse_minimal() { let url = Url::parse("http://localhost").unwrap(); assert_eq!(url.scheme(), UrlScheme::HTTP); assert_eq!(url.host(), "localhost"); assert_eq!(url.port_or_default(), 80); assert_eq!(url.path(), "/"); } #[test] fn test_parse_path() { let url = Url::parse("http://localhost/foo/bar").unwrap(); assert_eq!(url.scheme(), UrlScheme::HTTP); assert_eq!(url.host(), "localhost"); assert_eq!(url.port_or_default(), 80); assert_eq!(url.path(), "/foo/bar"); } #[test] fn test_parse_port() { let url = Url::parse("http://localhost:8088").unwrap(); assert_eq!(url.scheme(), UrlScheme::HTTP); assert_eq!(url.host(), "localhost"); assert_eq!(url.port().unwrap(), 8088); assert_eq!(url.path(), "/"); } #[test] fn test_parse_port_path() { let url = Url::parse("http://localhost:8088/foo/bar").unwrap(); assert_eq!(url.scheme(), UrlScheme::HTTP); assert_eq!(url.host(), "localhost"); assert_eq!(url.port().unwrap(), 8088); assert_eq!(url.path(), "/foo/bar"); } #[test] fn test_parse_scheme() { let url = Url::parse("https://localhost/").unwrap(); assert_eq!(url.scheme(), UrlScheme::HTTPS); assert_eq!(url.host(), "localhost"); assert_eq!(url.port_or_default(), 443); assert_eq!(url.path(), "/"); } }