commit ca61ad8b992289a5bd27b3f0795a0b59c86938fe Author: Rasmus Melchior Jacobsen Date: Mon Jan 30 09:48:57 2023 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..131a968 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "tiny-url" +version = "0.1.0" +edition = "2021" +repository = "https://github.com/rmja/tinyurl" +authors = ["Rasmus Melchior Jacobsen "] +license = "Apache-2.0 or MIT" +keywords = ["embedded", "url", "no_std"] + +[dependencies] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b00e4ff --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# A simple Url primitive +[![crates.io](https://img.shields.io/crates/v/tinyurl.svg)](https://crates.io/crates/tinyurl) + +This crate provides a simple `Url` type that can be used in embedded `no_std` environments. + +If you are missing a feature or would like to add a new scheme, please raise an issue or a PR. + +The crate runs on stable rust. + +## Example +```rust +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"); +``` + +The implementation is heavily inspired (close to copy/pase) from the Url type in [reqwless](https://github.com/drogue-iot/reqwless). \ No newline at end of file diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..64e7021 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,7 @@ +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + /// The url did not start with :// + NoScheme, + /// The sceme in the url is not known + UnsupportedScheme, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9a51da0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,185 @@ +#![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(), "/"); + } +}